Releases: crAK1644/orchestrator-mcp
Release list
v0.6.1
Two bounds that did not cover what they claimed to.
config_hash covered the agent table, not the block
config_hash is recorded with each consultation for provenance and compared by the dashboard's restart banner — the only way one process can tell the separate MCP server is still running an older file. It hashed the agent table alone.
Reviewers, roots, workflow bindings, spend ceilings and timeouts could all change without moving it, so the banner stayed silent on most of what the dashboard itself can edit, and a stored reply recorded the routing table that answered it while saying nothing about the ceilings it was answered under.
It now covers everything the MCP server reads at boot and cannot re-read — every field except dashboard:, which belongs to a process that re-reads the file on every request, so a port change there needs no restart and a banner demanding one would be wrong.
On upgrade the banner fires once. Widening moves every hash, so the first dashboard load after this finds a stored row that cannot match. That is not a false positive: a server still serving those rows is running the old code. Restart it and it clears.
consult.spend bounded reviews and workflows, not consultations
Three enforcement sites, none of them on orchestrator_consult. That tool takes a consultation_id and resumes the session behind it, so every call after the first spends another turn against the same paid CLI, and neither the review ceiling nor the workflow one was ever asked about it.
Two new optional ceilings:
consult:
spend:
max_cost_usd_per_consultation: 2.0
max_turns_per_consultation: 8Absent by default like the other four, so nothing changes for a config that does not set them. Checked where the public bind, the reviewer bind and the workflow bind converge, so one check covers every door — reviewers and workflow steps are consultations too, and are now bounded by their own ceiling as well as the ones above them.
A consultation with no turns is never refused: nothing has been spent, so no ceiling has been reached. What these stop is the turn after one. The refusal is checked before the turn is written, so it costs nothing and does not push the total further past the bound it is refusing to cross.
Set the turn ceiling as well as the dollar one if your routing includes an agent on a flat-rate plan — codex and antigravity report no per-turn price, so a dollar ceiling over them never leaves $0.00.
Also in this tag
No behavior change: three functions and one script that nothing in the source, the tests, the workflows or the packaging called were removed, and a lease test that asserted a wall-clock deadline no shared CI runner can honour now asserts only the renewal it is named for.
Install
pip install --upgrade orchestrator-mcp-server
brew upgrade orchestrator-mcp-server
v0.6.0
Breaking. Two configurations that started a server on 0.5.9 now refuse to start. Both failures name the offending value and exit from main(), rather than surfacing later as a traceback or a confusing runtime error. Read the two sections below before upgrading a running server.
Agent ids are validated where an agent arrives, not where one is written. An agent id is a YAML mapping key, so it is whatever the file says. The regex describing its shape lived in the dashboard — one of the two ways an agent arrives. The other way, an operator editing config.yaml or the managed agents.yaml by hand, reached every consumer without passing it: a scratch directory's name, the target_agent enum in the published tool schema, and a redirect header. The check now runs in load_consult_config, which is the layer where a mapping key becomes an id. An id must start with a letter or digit and use only lowercase letters, digits, dots, dashes and underscores, up to 64 characters; anything else is refused at startup with a message naming the key. Lowercase is enforced rather than normalized, because Foo and foo can be distinct keys — including across the two files that merge — so lowercasing one could collide with or silently become the other.
The same fix corrects an re.match that was never a whole-string test: $ also matches just before a trailing newline, so an id ending in one passed a check that was supposed to prevent exactly that. ConsultDashboard.delete interpolated the posted id straight into a Location header, which send_header writes verbatim. That redirect now percent-encodes the id rather than refusing it — the managed file is re-read on every request, so a key hand-edited in after boot is rendered with a delete button beside it, and refusing the id would leave the only control that can remove it unable to. Refusing the key at load is what stops it surviving a restart.
A roots entry that is not a directory on this machine fails at startup. consult.review.roots and consult.workflow.roots were checked only when a request used them, so a typo started the server, got the tools advertised, and reported itself on the first call that needed it — for review, after a reviewer had already been spawned. This is a hard failure rather than a dropped entry: an allowlist quietly shorter than the file says reads fewer trees than whoever wrote it thinks. The cost is one-sided and real — a machine that has not cloned one of several configured trees cannot start until the entry goes or the directory arrives. WorkflowConfig._expand_roots also gains the absolute-path check it never had; a relative root was being resolved against the server's own working directory, which is wherever the client happened to spawn it. The request-time checks stay, since a directory can be deleted while the server runs. config.example.yaml now ships roots: [] in both blocks instead of naming ~/src.
Release workflow — a comment correction only, no change to what the step does. It said the dist check "reads the files"; it globs dist/, counts two entries, and matches their names against the tag. Nothing opens a distribution.
Full changelog: v0.5.9...v0.6.0
v0.5.9
Documentation and release-pipeline changes. No behaviour change in the server itself.
README — the console script grew --help and --version in 0.5.8 and the README never said so. The install section now states that the MCP client spawns the server (which is the real answer to "how do I run this"), names the two flags that work outside a client, and adds a troubleshooting row pointing at --version for the case that sends people to a terminal: the client listing the server as failed, with no way to tell a PATH problem from a bad configuration.
Release workflow — the build job now checks the files it is about to hand over, not just the version that asked for them. The tag check reads pyproject.toml before uv build, and if-no-files-found: error only proves dist/ was non-empty; neither constrains what reaches PyPI. uv build does not empty dist/ first either, so a stale or mis-versioned distribution could ride along to an upload that cannot be withdrawn. The new step names the two files it expects and refuses anything else.
Two comments in that workflow also claimed more than the configuration does, and now say which is which: the pypi environment scopes identity but gates nothing, and id-token: write is job-scoped, which puts setup-uv and download-artifact inside the credential boundary alongside uv publish.
Full changelog: v0.5.8...v0.5.9
v0.5.8
Two fixes found after 0.5.7 shipped. No behaviour change to consultations, reviews or workflows.
A killed child hands its pipes back while there is still a loop. A subprocess transport finishes only when the child has exited and every pipe has reached EOF, and the paths through _terminate are the ones where no EOF is coming: a StreamReader over its limit has paused the transport, or the readers were cancelled. The transport was left to the garbage collector, and if the loop had closed by then, __del__ raised RuntimeError: Event loop is closed where nothing could catch it. _terminate now closes it in a finally. Verified not to cost any output: run_streaming's stop path returns the same stderr it did before, byte for byte.
The console script reads its arguments and its config errors. orchestrator-mcp-server --help used to print a five-frame traceback, because main read no arguments and went straight into a server booting against the caller's config. Now -h/--help and -V/--version answer, an unrecognised argument is refused with exit 2 instead of ignored, and a ConfigError leaves as one line naming the key to set rather than our own stack.
Full diff: v0.5.7...v0.5.8
v0.5.7
Token counts mean one thing on every runtime, and both spend ceilings hold at every door into a send.
Counting
- The usage parser names exact types instead of accepting subclasses. A subclass slipping past the parse ran its own
__lt__,is_integeror__int__inside helpers whose first promise is that they do not raise, and was returned in place of anint; the shape reporter would have written a name somebody else chose into a durable column. record_turnrefuses a non-intor negative count rather than letting SQLite coerce it. A caller passing"1"and"2"derived"12"and stored a row contradicting its own columns, stamped as counted the current way. Breaking for a caller that passed numeric strings, which never stored what it looked like it stored.- The codex cache caveat says what the two figures cannot both mean, instead of asserting which one is wrong.
Rollups
- Caveat chunks are sorted before tallying, so two refreshes of rows that did not change no longer return the same caveats in a different order.
- The legacy-semantics note fires on any group containing a legacy turn. It previously reported only mixed groups, leaving the commonest legacy group of all -- every turn legacy -- saying nothing.
- Workflow usage no longer borrows a consultation's step id for a review belonging to another workflow, and says so in the total when a workflow's spend names no step.
Links and ceilings
- The reviewer upsert keeps the first consultation link. A failure reported before the consultation existed carried nothing and erased the only pointer to a consultation already written and paid for: its turns stopped joining to the review, and deleting the review walked past its prompt and answer.
- A workflow step reads the workflow ceiling under the lease it is deciding with, and asks the review's ceiling before spending the step token a review step shares with its review.
- Running or retrying a workflow-owned review directly now checks the owning workflow's ceiling and holds that workflow's lease across the read and the send. A host holding the shared token could otherwise buy reviewer turns the workflow was charged for and never got to refuse. A direct run can now return
SESSION_BUSYwhile its workflow is busy elsewhere.
Historical rows are not migrated. Rows written under the old per-runtime meanings keep their numbers and carry the caveats that say so.
1149 tests passing.
v0.5.6
v0.5.4
v0.5.3
v0.5.2
v0.5.1 — workflows become first-class in deletion and the dashboard
0.5.0 shipped the workflow layer, but nothing else in the project had been taught that workflows exist. This teaches deletion and the dashboard about them, and fixes the one live defect that gap left behind. Nothing in the 0.5.0 consult, review or workflow tools breaks: no new write surface, no new runtime capability, no new network access.
A workflow-owned review can no longer be deleted out from under a workflow
ReviewStore._delete never looked at workflow_id, and request_delete_all snapshotted every review. Consultations were already protected from exactly this; reviews were not, and workflow_steps.review_id carries no REFERENCES clause, so the dangle was silent.
It was not only a dangle. _open_findings reads the review back through ReviewService.get, which answers a missing row with an error envelope rather than an exception — so the next fix round would have been handed an empty finding list and would have answered from the goal instead of from the review.
Both delete paths now refuse those reviews and point the caller at orchestrator_delete_workflow. request_delete_all excludes them from the snapshot, so the count shown is the count deleted.
Workflows can be deleted
Three tools, advertised only when workflow: is configured: orchestrator_delete_workflow, orchestrator_request_delete_all_workflows, orchestrator_delete_all_workflows. workflow_id is required — an omitted argument never means erase everything.
- Ids are snapshotted at request time, not re-selected at confirm time, with the same three refusals as the review path (not outstanding / expired / already spent), spent in the statement that reads them.
- A workflow that is not terminal is refused and pointed at
orchestrator_workflow_cancel. A step holding an unexpired lease is refused too: that lease may be a live agent process in another server, and deleting its rows would leave it writing into a workflow that is gone. - The whole tree goes in one transaction, and the review teardown is reused rather than copied.
Ownership is reviews.workflow_id and nothing else. parent_review_id is a link, not ownership: a caller's recheck of a workflow's review carries workflow_id = NULL, because orchestrator_review has no workflow_id argument. Walking descendants downward would sweep that recheck and its consultations into a delete no approval ever described, and would widen the set past the ids the token snapshotted — which is the whole reason ids are approved rather than a count. The surviving child's parent link is cleared before its parent row goes, so it lives on as a root of its own.
The dashboard can see workflows
/workflows and /workflows/<id>, with a monitor tile, a step timeline ordered by round_index, attempt, sequence, links from a step to its consultation and its review, and a back-link from a consultation to its workflow. Read-only throughout — deletion stays on the MCP tools, where the confirmation token lives.
Verification
943 tests, offline, against stub executables — no mocks and no network. Every guard has a test proven to fail without it: neutered, confirmed red, restored. That includes the two ownership tests and the invariant that a step's review_id and its review's workflow_id name the same workflow.
Walked by hand against a sqlite3 .backup copy of a real database, never the original, and a throwaway workflow created, cancelled and deleted in a scratch repository with every table checked for leftovers.
Reviewed twice through this project's own orchestrator_review. The first review found the descendant-walk defect that all 939 tests had missed; the recheck confirmed the fix and raised one completeness finding, which is on the record as open and answered with the invariant test rather than with a second definition of ownership.
Known follow-up
workflow_steps.review_id still carries no REFERENCES clause. The invariant holds by construction and is now asserted in tests, but the enforcing constraint is a migration on a shipped table and belongs to its own change.