Skip to content

Releases: WDahah/portable-dsh-multi-agent-plugin

v1.10.0

Choose a tag to compare

@WDahah WDahah released this 21 Sep 00:12
fa9f77a

A user reported that qualifying every route in a pool did not make those routes receive work. They were right.

advanced pool, all three routes qualified, 100 identical tasks:
  codex-sol      100
  claude-opus      0
  kimi-k3          0

The determinism is deliberate and remains the default: the same task with the same evidence always reaches the same model, which is what makes a run reproducible and an audit trail meaningful. The defect was that nothing said so, and orchestrator_capacity reported dispatchable: 3 — every number true, and a reader would still conclude that three models share the work.

Added

spread: true rotates work across every qualified route in a pool:

without spread  ->  codex-sol 100,  claude-opus 0,   kimi-k3 0
with spread     ->  codex-sol 33,   claude-opus 33,  kimi-k3 34

Keyed by run_id, so the same request still resolves to the same route — distribution without giving up reproducibility. It rotates only among routes that already passed every evidence rule, and never overrides review independence.

failover: true moves a run to a standby route when the first provider refuses. Deliberately narrow, requiring all three of: a refusal the provider issued before the child started, no output produced, and a read-only tool scope. Anything else records the attempt with its reason and fails rather than risking a repeated side effect. A run that moves is authorized by the new route's evidence, never the old one's.

A selection now also reports standby, selectionOrder, and a LOWER_PRIORITY_ROUTES_IDLE_UNTIL_FAILOVER warning whenever a qualified route is sitting idle.

Fixed

orchestrator_capacity now reports selects, idle, spreadWouldUse and failoverAvailable per pool, and marks each route selected or LOWER_PRIORITY_THAN_SELECTED. The documentation now states plainly that pools are priority-ordered rather than balanced.

Unchanged

Default routing is identical. All 238 role-and-variant outcomes from the previous build were replayed against this one and none changed. Both features are opt-in.

Verified

  • 168 tests pass on Linux, Windows, and macOS across Node.js 22 and 24.
  • A test asserting distribution and review independence together caught a real conflict during development: the first implementation let spreading rotate a review back onto the provider it was judging. Independence now outranks distribution.
  • A fresh clone of this tag passes manifest --check and verify (56/56 files).

v1.9.2

Choose a tag to compare

@WDahah WDahah released this 20 Sep 19:19
cd16e19

Two formatting blemishes in the demo output, spotted in a pasted transcript.

One override row was longer than the hard-coded padding width, so its arrow sat outside the column. The objective scene stacked three blank lines, because that material already begins with its own separator — correct when spliced into a prompt, redundant after a scene heading.

Cosmetic, but the demo is the first thing most readers see, and ragged output quietly contradicts a project whose argument is that it is careful about detail.

Fixed

Column widths are now derived from the longest entry rather than hard-coded, so adding a case later cannot silently misalign a table.

A test rejects trailing whitespace, stacked blank lines, and misaligned columns in the demo output, verified non-vacuous by reintroducing the narrow padding and confirming it fails.

No plugin behaviour changed. 157 tests pass on Linux, Windows, and macOS across Node.js 22 and 24.

v1.9.1

Choose a tag to compare

@WDahah WDahah released this 20 Sep 19:12
f5e214b

A reader ran the quickstart exactly as written and got a parser error.

PS C:\Users\WD> cd portable-dsh-multi-agent-plugin && node demo.mjs
The token '&&' is not a valid statement separator in this version.

&& is a parse error in Windows PowerShell, still the default shell on Windows. So the first command a new reader ran failed — on the one path the previous release existed to make smooth.

Fixed

The quickstart is now three separate lines, in both the README and the design notes:

git clone https://github.com/WDahah/portable-dsh-multi-agent-plugin
cd portable-dsh-multi-agent-plugin
node demo.mjs

A test rejects && in any shell block in README.md or docs/DESIGN-NOTES.md, verified non-vacuous by reintroducing the bug and confirming the test fails.

Why the pre-release check missed it

The fresh-clone verification before v1.9.0 passed, but it ran the steps separately under pwsh 7, which accepts &&. It verified that the steps worked; it never executed the literal line being shipped.

That is the same mistake as an earlier token estimate in this project: checking something adjacent to the claim and treating it as checking the claim.

Also

Adds the 1.9.0 changelog entry, omitted at that release.

No plugin behaviour changed. 156 tests pass on Linux, Windows, and macOS across Node.js 22 and 24.

v1.9.0

Choose a tag to compare

@WDahah WDahah released this 20 Sep 19:01
6df55dd

You can now evaluate the ideas in this project without installing a DSH/Cordis host.

Try it in ten seconds

git clone https://github.com/WDahah/portable-dsh-multi-agent-plugin
cd portable-dsh-multi-agent-plugin
node demo.mjs

demo.mjs runs the same selectRoute and parseVerdict the plugin uses in production. Only the qualification evidence is synthetic, and the demo says so on its first line — a demo that reimplemented the logic could agree with itself while the real thing was broken.

Seven scenes, each runnable alone (node demo.mjs diversity):

  • routing — five roles, each naming a pool and effort you can predict
  • refusal — no evidence means no dispatch, and the refusal names the exact probe that would fix it
  • expiry — stale, unavailable, and ambiguous evidence all refused
  • diversity — a review avoiding the model it judges, and the honest flag when it cannot
  • verdicts — prose and invented states refused; an unreadable verdict stops the loop
  • objective — the objective carried as fenced data, drift reported rather than inferred
  • aliases — twelve deprecated role codes still routing exactly as before

It shows refusals as prominently as successes, because refusing well is the point of the design.

Design notes

docs/DESIGN-NOTES.md records the reasoning behind the decisions, readable without installing anything: probes that can fail, why a same-model review is not independent, why the orchestrator must never judge the work, why hitting a cap is not success, and why retry is only safe when you know what already happened.

Every figure in it is measured. It also records the token estimate that was wrong and why, and the five additional agents that were rejected after measuring what they would cost — a design document that only lists wins teaches nothing.

Repository

Badges for CI, release, licence, Node version and zero dependencies. Issue templates that note refusals are often deliberate. A pull-request template that asks contributors to label projections as projections. A code of conduct.

Verified

  • 155 tests pass on Linux, Windows, and macOS across Node.js 22 and 24, including seven that keep the demo honest: CI fails if a scene stops running, if the synthetic-evidence disclaimer disappears, if a refusal stops being demonstrated, or if colour codes leak into piped output.
  • A fresh clone of this tag passes manifest --check and verify (55/55 files).

No behaviour changed in this release. The plugin itself is unchanged from v1.8.1.

v1.8.1

Choose a tag to compare

@WDahah WDahah released this 20 Sep 18:00
02cbf20

Three gaps that only appeared once the loop ran against real models rather than stubs. None was a correctness bug — the loop failed safe throughout — but each weakened observability or the audit trail.

Fixed

A reviewer answering only through the structured channel saved an empty answer. The verdict was stored and complete, but the record read back blank, and the review could never itself be reviewed (REVIEW_SUBJECT_EMPTY). The verdict is now rendered as the saved answer when the model supplies no text of its own; the parsed verdict remains the authority.

Deleting a run left any later review pointing at a record that no longer existed. orchestrator_forget now refuses with ASSIGNMENT_REFERENCED_BY_REVIEW and names the blocking reviews in referenced_by. Cascading would destroy the review and clearing the link would erase what it judged, so force: true accepts a dangling reference deliberately instead.

This fix is the design the plugin itself proposed during live testing: one model drafted it, an independent reviewer verified it, and it is implemented here as specified.

VERDICT_UNREADABLE did not say why. A reviewer cut off by a token limit and one that returned prose need opposite responses, and telling them apart meant reading the review run separately. Each cycle now reports reviewState and an unreadableCause of REVIEWER_HIT_TOKEN_LIMIT, REVIEWER_RETURNED_NO_USABLE_VERDICT, or REVIEWER_DID_NOT_COMPLETE.

Note

A reviewer needs room to think before it answers. A 2,048-token ceiling truncated a real reviewer before it emitted anything; the default of 16,384 was sufficient. The loop failed safe in that case — reporting no verdict rather than inferring one — but the cause was not visible, which is what the third fix addresses.

Documentation

The README now opens with a real verified run rather than a description of the integration, and states plainly that the plugin never judges the work, since every other claim is bounded by that.

Verified

  • 148 tests pass on Linux, Windows, and macOS across Node.js 22 and 24.
  • Covered: verdict rendering with and without findings, schema-only reviews readable and reviewable, deletion refused with the blocker named, force accepting a dangling reference while the review survives, and both unreadable causes distinguished.
  • A fresh clone of this tag passes manifest --check and verify (47/47 files).

v1.8.0

Choose a tag to compare

@WDahah WDahah released this 20 Sep 17:37
46f749b

A review now produces a decision the loop can act on.

Added

Declared verdicts. A reviewer returns verified, partial, failed, or needs-clarification, with onObjective, findings, clarifications, and the acceptance criteria it actually confirmed. Four states exist because a reviewer that can only pass or fail must guess when it lacks information; needs-clarification is the honest alternative, and clarifications is where an unstated requirement is named instead of invented.

Where the host's spawn provider supports it, the shape is enforced by the host, so a usable verdict does not depend on a model choosing to format JSON. verdict_source records which channel it arrived through.

orchestrator_iterate runs bounded revise cycles. While the declared verdict asks for more work, it reviews with a different provider where one is qualified, then revises from the findings:

cycle 1: reviewed by claude/claude-opus-5 (independent=true) verdict=partial  -> REVISING | revised by codex/gpt-5.6-sol
cycle 2: reviewed by claude/claude-opus-5 (independent=true) verdict=verified -> VERIFIED

The cap is 3, separate from and lower than the 8-round assignment limit, because each cycle is a full model call. Revise cycles may write files when those tools are allowed, and each records its own evidence link so an unreviewed write stays identifiable. Every exit is explicit: VERIFIED, NEEDS_CLARIFICATION, UNCONVERGED, VERDICT_UNREADABLE, REVISION_INCOMPLETE. Reaching the cap reports UNCONVERGED, never "done".

The plugin stores verdicts and never interprets them. It reads the declared state to decide whether another cycle is permitted; it does not read prose to judge whether work is acceptable.

Objectives travel as data to every child, and drift is reported by the reviewer as onObjective: false rather than inferred by comparing text.

Optional compaction on the economy pool condenses a long artifact between cycles. Off by default; measured saving 9% at 3,000 characters, 16% at 12,000, 18% at 24,000. It replaces working context only — the full revision stays readable and the final answer is never a summary — and a compaction that is not genuinely smaller is refused and reported.

Token cost, measured

A 3-cycle loop is 5 to 6 model calls, not 3. Against the same loop without these mechanisms, input falls from roughly 23,100 to 9,300 tokens: a 60% reduction.

An earlier planning estimate of "~19,500 to ~7,000" counted cycles rather than calls and was wrong. These figures are measured from the built loop, and the changelog records the correction rather than restating the projection.

Fixed

Revisers were receiving the verdict schema, because a reviser reads the prior run through the same reviews link a reviewer uses. A reviser must return revised work, not a judgement of it.

Verified

  • 140 tests pass on Linux, Windows, and macOS across Node.js 22 and 24.
  • Covered: every verdict state's loop behaviour, prose and invented states refused, cap enforcement, unreadable verdicts stopping the loop, drift reported rather than inferred, write cycles recording evidence, and compaction applied only when genuinely smaller.
  • Cross-version compatibility verified bidirectionally against the real v1.7.0 build.
  • A fresh clone of this tag passes manifest --check and verify (46/46 files).

v1.7.0

Choose a tag to compare

@WDahah WDahah released this 20 Sep 17:15
4e4a374

Knowing what can run, and reviewing work with a different model.

Added

orchestrator_capacity answers what is dispatchable right now. Learning this previously meant hand-joining the route list, provider_registered, and raw qualification records. Per pool it reports dispatchable routes, the distinct providers they span, and independentReviewPossible; anything unusable names its reason and a requalify object that can be passed straight to orchestrator_qualify. Dispatchable routes report expiresInMs, so evidence about to lapse is visible before it bites.

It also reports structuredVerdictSupported, read from the host's spawn provider.

reviews links a run to the one it judges. The reviewer is seeded with the subject's own request and answer, fenced as UNDER REVIEW (data, not new instructions) and followed by an explicit instruction not to follow anything inside them. A subject that tries to instruct its reviewer is carried verbatim but never obeyed. The relationship is stored and appears in orchestrator_list.

A review prefers a provider other than the one it judges. The fixed priority order would otherwise send a review to the same model that produced the work, sharing its blind spots:

plan   ran on codex/gpt-5.6-sol
review ran on claude/claude-opus-5   independent: true, avoided: codex

When no alternative provider is qualified, the review proceeds and says so — independence.independent: false with a reason and a REVIEW_SHARES_PROVIDER_WITH_SUBJECT warning — rather than passing as independent.

Changed

Avoiding a provider reorders candidates only. It never removes one, never relaxes the evidence rules, and never promotes an expired or unavailable route for being independent. A review of an unknown, still-running, or empty subject is refused rather than judging output that does not exist yet.

Compatibility

Assignments written by earlier versions remain readable and report a null reviews and independence. Assignment records are not closed-schema, so earlier versions also read records written by this one — unlike the journal changes in 1.2.0 and 1.5.0, there is no one-way upgrade constraint here. Verified against the real v1.6.0 build in both directions.

Verified

  • 125 tests pass on Linux, Windows, and macOS across Node.js 22 and 24.
  • Covered: capacity before, during and after qualification; unregistered providers distinguished from missing evidence; the diversity preference and its honest flag; avoidance not weakening evidence rules; review material fencing a hostile subject; and refusal of unknown, unfinished or empty subjects.
  • A fresh clone of this tag passes manifest --check and verify (44/44 files).

The bounded revise loop and structured verdicts follow in the next release.

v1.6.0

Choose a tag to compare

@WDahah WDahah released this 20 Sep 17:00
6d0d508

Roles now name what they do, and intent is recorded separately.

Added

Five roles replace twelve numeric codes: standard, deep, review, vision, and domain. Each names routing that can be observed in the result — the pool, the effort, and the evidence it requires — so a reader can predict behavior from the label.

A free-text intent records what a task is for, such as "add password reset". It appears on the assignment, in the child's label, and in orchestrator_list:

was:  R07 · claude/claude-opus-5 · high · round 1
now:  review: check the migration plan · claude/claude-opus-5 · high · round 1

Intent never affects routing. Two runs with opposite intents and the same role reach the same model. That separation is deliberate: a label that quietly changed the model would be a routing rule disguised as documentation.

Changed

The codes R01-R12 still work and route exactly as before, but are deprecated. A selection reports role, roleSupplied, and roleDeprecated, and adds a DEPRECATED_ROLE_CODE warning, so migration requires no guesswork.

Five of those codes — R02, R03, R06, R10, R12 — were never documented anywhere, and R03 and R12 silently reached the advanced pool at higher cost. Git history confirms the meanings were never recorded: the taxonomy arrived whole in the initial release from an upstream system whose documentation did not travel with it. Each code therefore maps to the behavior it already produced, rather than to a meaning invented after the fact. R08 and R09 were byte-identical, so both map to domain.

Verified

  • Behavior preservation was not assumed. All 84 role-and-variant outcomes (twelve codes across seven task variants) were captured from the previous build and replayed against this one: every outcome is unchanged.
  • 116 tests pass on Linux, Windows, and macOS across Node.js 22 and 24.
  • Covered: each name's declared routing, evidence-gated roles refusing with the correct remedy, every alias matching its canonical name across six variants, intent recorded without routing, invented names still refused, and risk/complexity/pool still overriding the role.
  • A fresh clone of this tag passes manifest --check and verify (43/43 files).

This is the first half of the v1.6.0 foundations; capacity reporting, review hand-off, and provider diversity follow.

v1.5.0

Choose a tag to compare

@WDahah WDahah released this 20 Sep 05:10
1e3141e

A saved run now answers what authorized it, not only which model answered.

Added

Runs record their authorizing evidence. Since 1.2.0 a record named the exact model, but not what permitted the run — so whether a run was authorized by evidence that had already expired, or whose attestation allowed a confidential task, could not be answered from storage. Delegations and direct tasks now carry an evidence block holding the evidence id, its own issue and expiry, the passed probe cases, allowed data classes, domain evidence, and the attesting operator when one widened policy.

The link is checkable rather than asserted. evidenceId is derived from the evidence itself — provider, model, effort, issue time, and issuing runtime — instead of being assigned. Recomputing it from the qualification a record names must reproduce the stored id, so an audit can verify the link.

Changed

Direct-task records may carry an optional evidence block, fixed at plan time. It sits at record level rather than inside the request, because it authorizes the run instead of forming part of the hash-covered request. The journal refuses any later addition, edit, or removal with EVIDENCE_MUTATED — a tampered link is rejected even with a recomputed checksum — and a malformed block is refused rather than silently ignored.

Records written by earlier versions remain readable and report a null evidence with a false evidence_recorded, rather than a fabricated link.

Upgrading

Reading is one-way, as in 1.2.0. This version reads journals written by 1.0.x through 1.4.x, but older versions reject journals written by this one, because their record schema admits no evidence field. Finish or abandon in-flight direct tasks before downgrading.

Verified

  • 105 tests pass on Linux, Windows, and macOS across Node.js 22 and 24.
  • Cross-version compatibility exercised against the real v1.4.0 build rather than a hand-written fixture.
  • Covered: id derivation and its sensitivity to each identifying field, both paths recording and reading the link back, runs without evidence reporting honestly, malformed links dropped, post-plan mutation refused, and unreadable shapes refused.
  • A fresh clone of this tag passes manifest --check and verify (42/42 files).

v1.4.0

Choose a tag to compare

@WDahah WDahah released this 20 Sep 04:47
ee182bc

The plugin now says what it already knew.

Added

Every refusal names the probe that would resolve it. An UNAVAILABLE selection previously gave a reason but no remedy, leaving the caller to infer which probe to run. Each candidate route now carries a requalify object holding exact orchestrator_qualify arguments — capabilities for a probe-backed requirement, and an attestation skeleton for domain or data-class policy that no probe can grant. A hint produced when no evidence exists covers everything the task needs, so following it yields evidence that actually satisfies the task instead of another refusal.

EXPIRED_QUALIFICATION also reports expiredAt and expiredForMs.

Token usage is reported where cost cannot be. Only 2 of 15 routes carry published pricing, so costUnknown: true was the entire story for the rest — close to implying the work was free. Direct tasks now report usage per round and as a task total, with usageRoundsMissing counting rounds that never settled, so a null total sits beside that count rather than a zero that would read as no consumption.

Delegated assignments report usage: null with usage_reason: "CHILD_RESULT_CARRIES_NO_USAGE". The host's child-agent result contract carries no usage, so the reason is named rather than a number invented.

Reserve routes are labelled. Four routes sit in no pool by design; orchestrator_inventory now marks them reserve: true, separating a deliberately held-back route from a broken one. The flag is derived from pool membership, so it cannot disagree with routing.

Compatibility

All additions are new fields; no existing field changed meaning, and no record format changed. Journals from 1.0.x onward remain readable.

Verified

  • 96 tests pass on Linux, Windows, and macOS across Node.js 22 and 24.
  • Covered: hints usable as literal qualify arguments, capability and attestation hints distinguished, expiry timing, an unpriced route reporting usage while cost stays unknown, an unsettled round reporting missing usage rather than zero, and the reserve flag staying consistent with pool membership.
  • A fresh clone of this tag passes manifest --check and verify (41/41 files).