Repository navigation
Releases: zanaat-dev/baron
Release list
v0.45.0 — mergeability, comments, run sha; Azure board check
Three additions to the contract an agent reads, and an Azure board check that now happens before anything is written.
Added
- Why a pull request can or cannot merge (#232).
scm.pr.status/baron_scm_read op=pr_statuscarriesmergeability:mergeable,conflicts,computing,blocked_by_policy,failedorunknown. Azure used to collapse its trial-merge status to a boolean, so queued, a policy refusal and a failure all looked the same. GitHub maps itsmergeable_state. A PR that is no longer open readsunknown, so Azure's lingering "succeeded" is never mistaken for "merged". The booleanmergeablestays for existing callers. - Reading an item's comments (#237).
baron_issue_read op=commentsand theissue.commentsrecipe op return every comment, oldest first, on Azure DevOps, GitHub, Jira and Linear. Read before posting so the same evidence is not added twice. - The commit a CI run built (#237). Runs carry
sha(AzuresourceVersion, GitHubhead_sha), so a run, and the deploy after it, can be tied to a merge commit.
Fixed
- Azure: a Kanban column the board does not have (#239). A backlog item asked for a column its board lacks used to read as reachable, and only Azure's refusal of the write caught it, past the point where
gapPolicy.separateBoardColumncould decide. The board is now read first: a missing column, or one whose state mapping disagrees with the target state, is reported before anything is written, and the board's own spelling of the column is what gets written.
v0.44.1 — first release from CI; resume fixes
The first Baron release published from CI over npm trusted publishing (OIDC), with provenance on every package. It also opens the new MCP Registry listing, io.github.zanaat-dev/baron.
Fixed
- Resuming a recipe run (
baron run --resume,baron_recipe_run { resume }) had two defects (#255):- An optional question left empty came back as
nulland failed the next step's argument check (#251). - Reads were replayed from the journal, so a
task-landstopped by pending checks kept seeing "pending" on every resume (#252). Reads made after the last completed write now run again. Earlier reads stay replayed, so no write is repeated.
- An optional question left empty came back as
v0.44.0 — Baron moves to zanaat-dev and @zanaat
Baron now lives at zanaat-dev/baron and publishes as @zanaat/baron-*.
Moved
- The repository moved from
loncadev/barontozanaat-dev/baron(#250). GitHub redirects old links. - npm packages are renamed from
@lonca/baron-*to@zanaat/baron-*(#254).@loncais the Lonca SDK's product scope, and Baron is a separate product. - The MCP Registry name becomes
io.github.zanaat-dev/baron. It goes live with the first release published from CI.
Upgrading
- Claude Code plugin:
/plugin marketplace remove baron, then/plugin marketplace add zanaat-dev/baronand/plugin install baron@baron. - Direct MCP config: replace
@lonca/baron-mcp-serverwith@zanaat/baron-mcp-server. - CLI:
npx @zanaat/baron-cli init(was@lonca/baron-cli).
The @lonca/baron-* packages stay on npm at 0.43.0 and will be deprecated with a pointer to their new names. Nothing is unpublished.
Fixed
docs/mcp.mdpointed at@lonca/baron, a package that never existed. It now says@zanaat/baron-cli.
v0.43.0 — Azure: expired policies and board columns, told honestly
Two Azure DevOps defects found by comparing Baron with the toolkit it was extracted from, fixed.
Beetegre-V2 — the project Baron came out of — kept growing its own Azure DevOps scripts after the
extraction, and two lessons it learned by measurement never flowed back. Both are in Baron now.
An expired branch policy no longer reads as one still waiting to run
Azure marks a branch-policy evaluation expired once the target branch moves past what it
evaluated, and it does not re-run an expired evaluation by itself. azure-devops-node-api's enum
deserializer knows six statuses and turns any other string into 0 — Queued — so Baron counted
an expired policy as pending, and task-land told you to wait for something that was never going
to happen. The evaluations are now read raw; an expired policy, or any status Baron does not
recognise, counts as failed with a remedy that names the policy and says to re-queue it, and
task-land prints that remedy. A 404 from the evaluations API reads as unknown, never none.
(#227, #228)
A board column is never dropped in silence
A role's board column was written only through an item's Kanban column field and skipped without a
word when the field was absent — which is every Bug and Task on a sprint Taskboard. Azure then
shows the card in the last column mapped to its state, so a card moved to Test read as
Waiting for Release with nothing verified.
- Taskboard columns are now written through the Work API, in the item's own sprint, after the state.
- An uncustomized Taskboard — no columns of its own, one per state — takes a column named like the
target state, and the state write places the card. - When an item cannot take its role's column at all (on no board, a column its Taskboard lacks for
its type, or a column whose own state mapping disagrees with the role's state), the core decides
before anything is written, bygapPolicy.separateBoardColumn:error(the default) refuses
withBOARD_COLUMN_UNREACHABLEand the reason;degrademoves the state alone and warns. - The contract gains an optional
IssuesTransport.boardColumnUnreachable, pinned by a new
conformance suite.
If your Azure policy maps board columns and some items sit on no board, transitions that used to
"succeed" with the column silently ignored now stop and say why. Set
gapPolicy.azure-devops.separateBoardColumn to degrade to keep the old outcome, with a warning.
(#229, #230)
What is next
The rest of the comparison is filed as #231–#239: role variants (two columns inside one state),
mergeability as an enum, re-queueing stale checks, arming auto-complete after review, a verified
pull-request ↔ work-item link, and the read side an orchestrator needs (open PRs, their files,
remote branches, @others).
v0.42.0 — what a day on a real Azure DevOps project found, fixed
What a day of running Baron on a real Azure DevOps project found, fixed.
0.41.0 was dogfooded end to end on a live project — issue, branch, a run that stopped halfway and
was resumed, pull request, merge, sync, close — with the agent driving Baron and a human watching.
Six defects came out of it. Five are fixed here; the sixth is filed with its shape (#223).
A merge is judged after Azure finishes it
Azure answers the pull-request completion request with the PR as it was — still Active — and
merges a moment later. Baron read that answer as the verdict and reported a real merge as
MERGE_FAILED; a user who believed it would have run task-land again against an already-merged
pull request. mergePullRequest now waits for Azure to finish (bounded, 20 seconds, re-reading
the PR each second), returns the merge commit, and — if the wait runs out — says how long it
waited and to read the PR status before retrying.
CI runs are filtered where the provider is
Listing Azure runs by branch fetched the newest N builds and filtered the branch afterwards, so
top was spent on other branches and a build queued a moment ago on the asked-for branch was
invisible: the same query returned nothing at limit 5 and the build at limit 50, and trace said
"no CI run" about a build Baron itself had just triggered. The branch now reaches Azure as a
filter and results come newest-queued first, so runs still queued or running count as the latest.
trace finds the pull request's own builds
Providers build a pull request on a ref of their own — refs/pull/<n>/merge — not on the source
branch, so a trace that asked by branch could never see a PR's validation builds. The ci contract
learns the join (RunQuery.pullRequestId, Run.pullRequestId; the conformance suite pins it), the
Azure adapter maps it to the merge ref, trace asks for the branch and then the pull request, and
when neither has a run it names both places. baron_ci_read op=runs takes pullRequestId too.
The post-merge build on the target branch is not joined yet; #223 says what that needs.
A failed run says where it stopped, and what it had done
Over MCP, a failed recipe run carried its run id only in structuredContent, which the model does
not reliably see; the agent reported "no run id" and read the journal by hand. And a resumed run
that had replayed three steps and then failed returned the error alone — its "Replayed …" lines
were gone, and the agent concluded nothing had been replayed. The error text now ends with the run
id and the exact resume call, and the messages the run produced before it stopped arrive as the
second text block, exactly as they would on success. A provider's own error is wrapped as
RECIPE_STEP_FAILED so it carries the same.
init keeps the credentials template committable
A hand-written .baron/credentials.* in a project's .gitignore also matched
credentials.example, so the template init means to be committed never reached the repository.
init now evaluates the ignore patterns against the template the way git does and appends
!.baron/credentials.example when something would hide it.
What Baron got right on the day
Across three failed resume attempts of the same run nothing was repeated and Azure ended with one
pull request; trace's missing reasons were read correctly by the agent; and when Azure did not
close the item on merge, task-sync-report named it and task-move closed it.
v0.41.0 — resumable runs, trace, an honest handshake
A recipe that fails halfway is now a retry, not a cleanup job; one read says where an item is end
to end; and the server's handshake lists what it actually speaks to.
Every run is journaled and resumable
ship and task-finish mutate several providers in sequence. When the third step failed, the
first two had already happened, and running the recipe again from the top did them again — the
concrete case was a second pull request.
Every run now appends one line per event to .baron/runs/<runId>.jsonl (gitignored; init adds
it): the inputs it started with, each answer, each do step with an idempotency key and the result
it bound, its messages, and the error or the end. The run id is reported on success and on failure
— the CLI prints the exact command to continue, the MCP error carries details.run.
baron run --resume <runId> and baron_recipe_run { resume } restore the inputs and answers
(nothing is asked again), replay every completed step from the journal instead of executing it, and
carry on from the step that stopped. A step's key includes its interpolated parameters, so changed
inputs run again; a changed recipe refuses to resume (RUN_RECIPE_CHANGED); a finished or unknown
run is RUN_NOT_FOUND.
Proven live on Baron's own repository: a recipe that created an issue, commented on it and then
opened a pull request from a branch that did not exist stopped at the PR twice and was resumed
twice; GitHub ended up with one issue, one comment and one pull request. Compensation — undoing
what a run that cannot be completed already did — stays deferred until a real case gives it a shape.
trace: where one item is, in one read
issue.trace in a recipe, baron_issue_read op=trace over MCP: the issue, its canonical branch,
the most recent pull request from that branch in any state with its review and checks, the latest
CI runs on the branch, and the most recent deployment whose ref is the branch — composed from the
ports already bound, joined on the branch name the core already derives. A part that could not be
filled is null and named in missing with the reason: the port is not bound, or the item has
not reached that hop. The tool count is unchanged.
The handshake names every provider and recipe
The server's instructions said "Azure DevOps, GitHub, Slack" and five recipes. Both lists are now
built from the registries, and a test pins that every provider and every built-in recipe appears.
v0.40.0 — Linear browser sign-in, Jira sprints, init recovery, the gate proven
A browser sign-in for Linear that works on a bare install, Jira sprints, a baron init that
recovers from a wrong credential, and the gated-transition contract proven against a real classic
workflow.
0.39.0 was tagged but never published to npm. Everything it would have carried is in this release.
Linear: sign in from the browser, no key to paste
baron init --provider linear now offers to sign you in through the browser instead of asking for
a personal API key — through Baron's own public Linear application, so there is nothing to
register. It is the OAuth authorization-code flow with PKCE: no client secret exists anywhere, and
the token exchange is bound to the process that started the sign-in. The access token Linear issues
lasts a day; the refresh token, its expiry and the application id are stored beside it in
.baron/credentials, and the transport renews the token before it expires (or once, when Linear
refuses it) and writes the rotated pair back — from the MCP server and from baron run alike — so
a sign-in outlasts the day. BARON_LINEAR_CLIENT_ID selects another application; set it to empty
to keep pasting a key, which is narrower than any OAuth scope.
Two things the live run taught, both fixed before shipping: Linear matches the redirect URI
against the application's registered list exactly, port included, so Baron listens on a fixed
port (41765, BARON_LINEAR_CALLBACK_PORT moves it, and a port in use names the way out); and the
page the browser lands on after approving now says who signed in and what to do next, instead of
one bare line of text.
Jira stays on the API token: Atlassian's OAuth requires a client secret and offers no PKCE, so a
shipped CLI cannot hold it honestly.
Jira sprints
Sprints on Jira belong to a Scrum board, not to the project, and live on the Jira Software API
rather than the platform one — which is why the adapter shipped with sprints: false. It now
resolves the project's first Scrum board (or the one JIRA_BOARD names, by id or name), lists that
board's sprints as iterations with the active one as @current, moves an issue into a sprint
through the agile API, and filters a query by sprint. The sprint an issue is in is read from Jira's
sprint field, whose id varies per site and is discovered once from the field catalogue. A project
with no Scrum board simply has no sprints.
baron init recovers from a credential the provider refuses
When the provider cannot be read with the credentials just gathered, init says which provider
failed and why, and offers to re-enter that provider's credentials on the spot (current non-secret
values shown so a typo is visible; an empty answer keeps what is there; secrets never echoed),
saves them, and tries again. A --force run, a declined offer, or a second failure stops with
INTROSPECTION_FAILED naming .baron/credentials and the keys to fix. Jira names the site on a
network failure instead of a bare "fetch failed".
Also: init no longer announces "Provisioned N workflow labels" on a provider whose labels exist
by being used. IssuesPort.ensureLabels now resolves to whether anything was provisioned, and the
conformance suite pins the honest answer.
The gate, proven on the classic workflow
A company-managed Jira project on the classic jira workflow gave what the team-managed default
could not: a transition carrying a screen and a status that refuses hops. Through the MCP server,
Resolve without fields was refused with TRANSITION_FIELDS_REQUIRED naming resolution and its
accepted values with nothing written; the retry with fields landed; and a move from Closed was
refused with TRANSITION_NOT_PERMITTED naming Reopened as the one hop allowed. Nine checks, nine
passes, no code change needed.
What was run before publishing
The whole path as a user would — a publish-equivalent tree from a throwaway copy, a fresh
project, a private throwaway repository, init and doctor, every task-* recipe, the MCP
server over JSON-RPC, a headless Claude Code session with the plugin skills, and the Docker image.
The Linear sign-in was then run live against a real workspace: approval, renewal before expiry,
renewal on refusal, a steady-state run that rotated nothing, and a bare install with no
environment at all.
v0.38.0 — Jira Cloud, validated as a user would
Baron now speaks Jira — and this release was run the way a user would run it before it shipped.
Jira Cloud
npx @lonca/baron-cli@latest init --provider jira. Issues on Jira, branches and pull requests on
GitHub, one contract. Jira is the provider the role contract was hardest to fit, and the reason it
now fits: a status cannot be set on Jira. The workflow permits some transitions from the issue's
current status, and a transition may carry a screen that demands fields. The adapter reads both
from Jira's transitions endpoint, and the core verifies against them before writing —
TRANSITION_NOT_PERMITTED names what the workflow would accept, TRANSITION_FIELDS_REQUIRED names
every field a screen wants, and the caller passes them back as fields.
Roles key on the status name, so a confirmed policy.json reads "In Review" rather than an
id. Jira's three categories (To Do / In Progress / Done) cannot tell in_review from
in_progress, so baron init proposes from names and you confirm. Sprints are declared
unsupported for now (they live on the Jira Software agile API); the gap policy decides. The
setup walkthrough is transcribed
from a real run.
Found by running it as a user
Before publishing, the whole path was driven from a publish-equivalent tree on a fresh project —
init, doctor, every task-* recipe with Jira issues and a private GitHub repository, the MCP
server over JSON-RPC, a headless Claude Code session using the plugin skills, and the Docker image.
It found four things the test suites could not, all fixed here:
task-landno longer dies on a private repository with a fine-grained token. The check-runs
403 had been translated into aCredentialPermissionErrorbefore the fallback to Actions runs
and commit statuses could see it, so the fallback never ran. Public repositories hid this: their
check runs are readable without any permission. It now warns with the exact remedy and lands.task-sync-reportreports "merged but still in review". Jira does not close an item when its
PR merges, so that is the ordinary aftermath of every land there — and the sweep was silent about
it. A third drift class, auto-fixable withtask-movetodone.- The proposal reads Jira correctly. Its unordered statuses handed
in_progressthe review
state, and a "Subtask" listed before "Task" took thetaskrole. Fixed in the core, with the real
site's introspection as the regression test. - A wrong
JIRA_SITEis refused by name, with the shape it wants, instead of a raw fetch error.
A host without a scheme is accepted.
Also: baron_native_request works on Jira, and the transition tool no longer promises that stray
fields are ignored — Jira refuses a field its screen does not carry, and that refusal is right.
For contributors
Under pnpm 11, pnpm deploy rewrites the workspace manifests in place; RELEASING.md says to deploy
from a throwaway copy, and how the user-journey pass is repeated.
v0.37.0 — a transition can ask for fields
Baron's role contract now covers the provider it was hardest to fit, and a credential form can no
longer hide a credential file.
A transition can ask for fields
Jira cannot be told a status. You read the transitions its workflow permits from the issue's current
state, perform one, and answer whatever its transition screen demands — resolution, fixVersions
— fields that have nothing to do with the role. The first half (reachable targets, refused with
TRANSITION_NOT_PERMITTED naming what the provider would accept) shipped in 0.34. This release adds
the second.
A transport can now report the fields a move demands, each with required and any allowedValues.
The core checks presence before writing and refuses with TRANSITION_FIELDS_REQUIRED naming
every missing field at once, so a caller asks once and retries rather than learning them one
provider rejection at a time. It checks presence and nothing else: what a field means, and whether a
value is acceptable, stays the provider's.
// baron_issue_move { op: "transition", id, role: "done" }
// -> isError, code TRANSITION_FIELDS_REQUIRED
// structuredContent.details.fields:
// [{ "name": "resolution", "required": true, "allowedValues": ["Fixed", "Won't Do"] }]
// baron_issue_move { op: "transition", id, role: "done", fields: { "resolution": "Fixed" } }
// -> movedRecipes take the same fields on issue.transition. Both transport methods are optional, so a
provider that asks nothing implements nothing, and a recipe written for Jira runs unchanged on
GitHub. The in-memory conformance provider now refuses the way Jira does, so the suite can tell a
core that checked from one that got lucky. With this, Gate 3's contract work in the roadmap is done;
the Jira adapter is the first thing to build on it (#173).
Refusals carry data, not only prose
BaronError gained details, and the MCP server puts it in structuredContent. A
TRANSITION_NOT_PERMITTED lists the permitted targets; a TRANSITION_FIELDS_REQUIRED lists the
fields. An agent branches on the payload instead of regexing the message.
A blank credential no longer masks the file
Hosts that present credentials as a form — Docker Desktop's MCP Toolkit among them — pass every
declared variable whether or not the user filled it in. A blank GITHUB_TOKEN used to win over the
value sitting in the mounted project's .baron/credentials, and the server failed for want of a
token it had. An empty variable is now a gap the file may fill; a non-empty one still wins.
This is the groundwork for Baron's Docker MCP Catalog entry, submitted as
docker/mcp-registry#4925. Once it is in, Docker
builds and hosts the image as mcp/baron; docs/mcp.md describes the Toolkit path.
The update notice speaks in both directions
The stale-companion check reported a plugin older than the server and said nothing when it was
newer. 0.36.0 disproved the reasoning behind that: skills ship through the plugin marketplace and the
recipes they call ship through npm, so a plugin ahead of its server is the ordinary state between two
updates — and a 0.36 skill calling a recipe a 0.35 server has never heard of fails with an error that
explains nothing. Both directions now speak, with opposite remedies.
For contributors
The toolchain moved to pnpm 11. Developing needs Node 22+; the runtime floor for the published
packages stays Node 20.
v0.36.0 — a recipe can sweep
Baron's claim is that a workflow is enforced by the engine, not by an agent behaving well. This
release closes the hole in it.
A recipe can walk a list
The grammar was ask / do / require / message — every one single-shot. So a workflow that
sweeps N items could not be written at all, and the one sweeping workflow Baron ships (task-sync)
lived as prose in a skill, improvising primitive calls.
On an installation that sets policy.mutations.channel to recipe-only that prose could fix
nothing: every mutation it prescribed was refused, and the refusal told the caller to "find the
recipe that covers this" — which did not exist.
- for_each: ${inProgress}
as: item
collect:
as: mergedButOpen
from: "${item.key}"
when: { truthy: "${merged.id}" } # collect the ITEM, decide on what the PROVIDER answered
steps:
- do: scm.pr.find
as: merged
with: { sourceBranch: "${item.branchName}", state: merged }Three refusals came with it, each a closed footgun rather than a preference:
- Iteration bindings do not leak. Reading
${merged}after the loop gets nothing, not whichever
element happened to be last — a bug that reads as data, since the value is real and merely wrong. - A non-list is an error, not an empty run. Quietly doing nothing is how a sweep reports "all
clear" for a board it never read. - No
askinside a loop, no loop inside a loop. Asks are hoisted so a caller can supply them
upfront, which is what makes a recipe runnable in one shot; an ask in a loop is invisible to that
and asked once per element.
task-sync has recipes to run
Reading what the skill actually needed changed the shape of the fix. It detects, shows a table, asks
for a batch confirmation, then applies — and a recipe runs one-shot with its inputs supplied upfront,
so a confirmation that depends on what the sweep found cannot be a recipe step at all.
The split that does work is the one the architecture already wanted: the drift rule goes in YAML,
where the engine enforces it; asking a human stays in the skill, which is the part only an agent can
do.
task-sync-report— sweeps and reports. Read-only by construction, not by promise: there is no
mutating step in it. Reports items stillin_progresswhose branch has a merged PR, and items
in_reviewwith no PR at all.task-reconcile— clears a role label the provider's own state contradicts. One step, with the
guard that matters:reconcilefollows the provider whiletransitioncommands it, so it
refuses on an item the provider never closed rather than clearing a label on the strength of
nothing.- The class-A fix needed no new recipe: it is a forward move, which
task-movehas always done.
Class B — in review with no pull request — is reported and never fixed. A wrong branch, a force-push
and a hand-moved card look identical there and want different answers.
Verified against a live Linear install under recipe-only, the exact policy that used to make this
dead. That run also caught a wording bug before release: the "no drift" line fired over six drifted
items, because a condition holds one key and cannot AND two findings. It now says which finding it
speaks for.
Install: npx -y @lonca/baron-cli@latest · npx -y @lonca/baron-mcp-server@latest
Claude Code: /plugin marketplace add loncadev/baron then /plugin install baron@baron
The skills ship through the marketplace and the recipes through npm. Update both — a plugin newer
than its server holds skills calling recipes the server does not have yet.
Full diff: v0.35.0...v0.36.0