Repository navigation
Releases
consort 0.11.0 — Codex account failover, Termscape account discovery
Added
- Codex account failover:
CONSORT_CODEX_HOMES. A:-separated list of
CODEX_HOMEdirectories, one per Codex account, tried in order. A call
moves on only when codex reports a usage or rate limit in its own error
events or stderr (the exec transport now runs--json); the transcript is
never read for it. Any other failure stops the walk.@defaultand
@termscapeexpand to the inherited home and to every Codex account
Termscape (nodeterm) has signed in (system account, then
~/.nodeterm/cx/<sha256(userData\0id)[:16]>). One stderr line per attempt
names the home that ran. A home list forces the exec transport (the
plugin broker is pinned to one account) and refuses an explicit
CONSORT_CODEX_BACKEND=plugin. Delegation fails over only when the
limited attempt did no work at all (no item in codex's event stream), so
a half-applied edit or a leftover background process never reaches a
second account. Keyring-backed logins (noauth.json) are recognised via
codex login status. Only error events and a work-item count are kept
from the stream; the transcript never touches disk. The Codex probe walks
the same list. Tests:scripts/codex-backend.test.sh(stub codex). - Hardened by the gate on this change (Codex, Gemini via Pi, blind security
agent): the first cut fingerprinted the work tree withgit add -Ain a
throwaway index, which ran the repo'score.fsmonitor, wrote untracked
files into.git/objectsand missed edits to ignored files; replaced by
the no-work rule above. A malformed Termscapesettings.jsonno longer
echoes its offending line (it can hold an API key); a refused call clears
a stale result file; the walk and the probe survive a caller's
set -euo pipefailwithout|| true.
Fixed
codex execcalls without a payload and the Codex probe now read stdin
from/dev/null; before, a call with no payload could sit on "Reading
additional input from stdin..." when it inherited an open pipe that never
closed (a harness or a background job).
consort 0.10.0 — principal review pass and test suite out of the authoring session
Changed
- The principal's review pass runs in a non-inheriting agent, not in the
authoring session. Newagents/code-reviewer— same shape as the
security side-agent: Read/Grep/Glob, no shell, fed the diff file, the pack
paths and the test summary, returning findings JSON. It runs the
blast-surface sweep with repo access and is the "Claude only" column of
the merge. Why: a review pass is 30 to 40 tool calls, and every call
re-reads the whole context it runs in. In an authoring session at 200k to
400k tokens that pass alone cost more than every other leg combined, and
it was the one leg nobody measured — the security side-agent reports ~110k
tokens over 19 to 32 calls for its whole pass, the principal's had no
boundary to report from. The review skill and/consort:reviewnow have
the session gather mechanical evidence (diff file, scanners, tests), spawn
both agents and the panel in parallel, merge, verify and present; it no
longer reads the packs or the diff into its own context.
Added
scripts/consort-test.sh [base]— blast-surface step 5 as a tool
instead of a chore in the session. Runs the suite (CONSORT_TEST_CMD, or
detected fromtest.sh,package.json, pytest markers,go.mod,
Cargo.toml, a Makefiletest:target) on head and in a detached
worktree of the base ref (dependency dirs symlinked so a fresh checkout can
start), normalises failure lines across pytest / jest / go / cargo / TAP,
and prints ONE summary line plus the new failures (capped,
CONSORT_TEST_MAX_LINES) and the run directory. Full logs and a
result.jsonstay on disk for the code-reviewer agent. Exit 0 nothing
new · 1 new failures, or a red head against a green baseline whose
failures the parser could not read · 2 did not run (no command, bad ref,
head timed out underCONSORT_TEST_TIMEOUT) · 3 head ran but the
baseline could not — the last two are reported as such, never as green.
Run directories are owner-only;CONSORT_TEST_DIRgets the panel's
ownership and writability checks. Pinned byconsort-test.test.sh(30
checks: baseline diffing, no-baseline mode, log-on-disk output budget, the
cap, the crash case, refusals, the parser).
consort 0.9.1 — merge clusters the same defect across files
Fixed
merge-findings.mjsclusters the same defect anchored on different
files. Two reviewers can pin one defect to two files — one on the script
that fails to prune, the other on the unit file being deleted — and
file+line clustering showed it as two "only" findings, understating
agreement (measured on kvart PR #18: the panel's union was reported as
1 agreed + 2 "only" where it was 2 agreed). A finding now also joins a
cluster when its title names another member's file (last two path
segments, or a bare basename of at least 8 characters, sorun.pyin a
title is not a link); the detail is not used because it routinely lists
neighbours, and word overlap was rejected because the real case shared one
content word. Such clusters are taggedcross-file. One reviewer's two
findings never merge, as before.- Hardened by the gate on that change (Codex, Gemini via Pi, blind security
agent): every member of a cluster is printed under its representative, so
a cluster can no longer delete a reviewer's text from the report; the
title match requires a whole path token (subtasks/run.pydoes not name
tasks/run.py) and a key that looks like a file (an extension or a path,
at least 8 characters — a one-wordfilesuch asauthorizationcannot
act as a keyword magnet); files compare as paths, not through the
punctuation-stripping normaliser (src/foo-bar.jsandsrc/foo/bar.js
are distinct); a finding that relates to several clusters merges them
when they share no reviewer, so the outcome no longer depends on argument
order, and prefers the cluster it names explicitly over the one it merely
sits near when they cannot merge; reviewer file strings no longer appear
in the tag; a set is capped at 500 findings with a loud stderr line.
Second round (Codex, Gemini via Pi): path tokens are extracted from the
title and matched as whole-segment suffixes of the other file's path (a
full repo-relative path and a trailing period both work now); a bare
basename shared by two different paths in the merge names nothing; a
finding joins the largest compatible subset of its candidate clusters
(no reviewer twice, span within the proximity even after a merge) rather
than only the first, so non-disjoint candidates resolve the same way in
any argument order; members keep arrival order through merges so the
tie-break "earliest set" holds. Still greedy, by construction and now
documented: one finding relating to two findings of the same reviewer
joins the earlier; proximity grouping within a file follows arrival order.
consort 0.9.0 — review panel: several cross-vendor reviewers on every diff
Added
- Review panel — several cross-vendor reviewers on every diff, chosen
withCONSORT_REVIEWERS.scripts/consort-panel.sh [base]runs
consort-review.shonce per leg (codex[:model],gemini[:model],
pi[:provider[:model]]; e.g.codex,pi:google-vertex= the Codex CLI
plus Gemini through Pi), in parallel, each under its own backend env, and
writes one findings file per leg plus a manifest (label, backend, model,
status, exit, seconds, file). A leg that fails, returns nothing, or runs
pastCONSORT_PANEL_TIMEOUT(default 1800 s; the leg's whole process
group is killed) fails the panel (exit 3) with that leg's file emptied —
the other legs' results stay on disk, but the configured panel did not
run and the caller is told so; an all-excluded diff propagates as exit 4;
stale leg files from a previous run are removed first. Every leg's stderr
is relayed with a[label]prefix, so the backends' evidence lines stay
visible. A provider-pinned Pi leg with no model unsets an ambient
CONSORT_PI_MODELso the provider default applies. Bash-native wall-clock
cap (notimeoutbinary needed);scripts/consort-panel.test.shpins the
grammar, env plumbing, hard-fail, exit-4, timeout and stale-file rules
against a stub reviewer. merge-findings.mjsmerges any number of findings sets. Arguments
are<principal.json> <reviewer.json>...(labels from basenames or
label=path); findings cluster across all sets by file and line
proximity, one finding per set per cluster, the representative being the
most severe member. Sections: caught by more than one reviewer (tagged
with every reviewer that caught it, sorted by agreement then severity),
each reviewer's "only" section, then the principal's. A file that is
missing, empty,{}or{"findings":null}is reported as NO RESULT
in the report and exits 3 — previously it merged silently as an empty
(clean) set.scripts/merge-findings.test.mjspins the contract.- Pi backend honours consort-scoped Google settings for google
providers, exported to the Pi process only and winning over the ambient
Google variables when set:CONSORT_GCP_PROJECT→GOOGLE_CLOUD_PROJECT,
CONSORT_GEMINI_LOCATION→GOOGLE_CLOUD_LOCATION,
CONSORT_GCP_CREDENTIALS→GOOGLE_APPLICATION_CREDENTIALS. The
credentials path must be a readable file or the backend refuses to start,
so a typo cannot fall through to whatever ADC the shell holds. Lets a
static config keep a Gemini leg alive where the user's ADC is an
hourly-expiring workforce token.
Changed
/consort:review, thereviewskill and the lifecycle skill call
consort-panel.shand merge every leg; the presented order is now
"caught by more than one reviewer", each reviewer's "only" section, then
the principal's (was: both / Claude only / Codex only).- Hardened by the gate before release (Codex, the blind security agent and
the principal, on the panel's own diff): each panel run gets a fresh,
exclusively created subdirectory ofCONSORT_PANEL_DIR(concurrent
panels sharing a parent no longer corrupt each other; a stale manifest
can no longer read as the current verdict), the parent must be owned by
the caller and not world-writable, every file is created with noclobber
so a planted symlink is refused rather than followed, leg start times
stay in memory (a stamp read back from disk was substituted into bash
arithmetic, which expands array subscripts — command execution for anyone
who could write the file), exit codes read from disk are validated as
integers, the manifest is built by a real JSON serializer, reaped pids are
cleared so a recycled pid is never signalled, cleanup is armed before the
first leg starts and escalates TERM→KILL, a base ref starting with-is
rejected (both in the panel and inconsort-review.sh),/consort:review
quotes its argument,merge-findings.mjsstrips control characters from
reviewer text, renders a severity outside the schema enum asunknown,
and opens the report with a data-fence line. Pi backend:
CONSORT_GCP_CREDENTIALSis resolved to an absolute regular file before
the run changes directory, refused for workspace-write runs (an unfenced
shell must not have a long-lived key in its environment), a run whose
final message stopped at the output token limit is discarded as truncated,
the JSON extractor says why it found no result, andCONSORT_PI_RAW
captures the raw event stream as evidence. Second gate round (Codex + Gemini
via Pi on the hardened diff): group-writable parents refused and run
directories created 0700, legs resolving to the same backend+model refused
(one reviewer twice is not agreement), empty spec fields rejected, HUP and
EXIT covered by the cleanup with KILL sent only to a group that still
exists, expired legs stopped together, a leg whose leader dies from outside
has its group swept, the merge no longer chains clusters past the
proximity, a Google key file inside the workdir is refused (the fence would
let the reviewer read it), the workspace-write refusal covers the ambient
GOOGLE_APPLICATION_CREDENTIALStoo, andCONSORT_PI_STDERR/CONSORT_PI_RAW
must be owned regular files (made owner-only; symlinks refused). Deferred, pre-existing: a
repo-local.claude/rules/is auto-injected into every reviewer's prompt
(0.4.0 design), which a hostile repo could use to talk every leg into an
empty result at once — opt-in or fencing is tracked as follow-up.
consort 0.8.0 — Pi backend, any vendor through one CLI
CONSORT_BACKEND=pi drives the Pi coding agent (@earendil-works/pi-coding-agent >= 0.84.0) for any provider: Anthropic, OpenAI (API key or ChatGPT/Codex subscription OAuth), Google Vertex (ADC / service-account key) and more, behind one headless JSON-lines protocol. Attestation is native: every assistant message names its provider, model and usage (plus the server-reported model where an adapter has it); a run served by anything but the requested pair, or marked failed, is discarded. A fence extension (scripts/pi-fence.mjs) bounds Pi's tools to the workdir, mirroring Pi's own path normalisation (@, file://, ~, unicode spaces, read-path variants, symlinks incl. dangling); read-only refuses bash/edit/write; workspace-write is opt-in because Pi has no OS sandbox. anthropic as provider is refused unless the principal is stated not to be Claude. Every run is --offline; rg/fd must be resolvable up front; results are validated against the schema. Measured on a 25-file diff with 5 known defects: same gpt-5.6-sol through Pi used a third of the tokens of the Codex CLI and found the HIGH, with lower recall (2/5 vs 4/5); Gemini 3.1-pro through Pi found the HIGH that gemini-cli missed. See CHANGELOG 0.8.0 and PR #4.
consort 0.7.1 — Gemini cli transport pins the served model
gemini-cli (0.58.0, Vertex auth) rewrote any model id ending in flash to its own flash default: CONSORT_GEMINI_MODEL=gemini-3.8-flash reviewed as gemini-3.5-flash and only the CLI's session log said so. The cli transport now enables the CLI's pass-through resolver through a throwaway system settings directory (system-defaults path pinned, real system settings copied JSONC-tolerantly, fail-closed), runs every call with --output-format json and discards any run not served solely by the requested model; one stderr evidence line per call (tool calls, tokens). Stderr capture is resolved before the cd and created owner-only without a process-wide umask. See CHANGELOG 0.7.1 and PR #3.
consort 0.7.0 — lean backends
Changed
- Codex exec transport runs read-only calls with
--ignore-user-config,
reasoning effort pinned viaCONSORT_CODEX_REASONING(defaulthigh). The
reviewer no longer inherits~/.codex/config.tomlplugins, plugin hooks,
MCP servers ordeveloper_instructions. Measured on a 25-file diff: a stock
config whose oh-my-codex prompt hook matched "parallel mode" inside a rule
pack fanned the review into three subagents and re-prompted from its Stop
hook — 10.7M input tokens, 909 s; the bypass reviewed the same diff in 2.46M
tokens, 495 s, with the same real findings. Workspace-write (delegation)
keeps the user config, where sandbox narrowing and approval policy live;
CONSORT_CODEX_USER_CONFIG=1keeps it for read-only calls too. A CLI
without the flag falls back to the stock config with a stderr warning.
(Gate: a free-formCONSORT_CODEX_ARGSpassthrough was dropped before
release — env-supplied argv after-scould have revoked the sandbox.)
Added
- Opt-in diff path excludes in
consort-review.sh(CONSORT_DIFF_EXCLUDE,
colon-separated git pathspec globs matched from the repo root) for repos
that commit generated artefacts. Nothing is excluded by default: lockfiles
stay in, as the supply-chain rule requires. Excluded paths are listed on
stderr and handed to the reviewer as fenced data; an all-excluded diff
exits 4 instead of passing as clean. The diff is now taken from the repo
root (:/) regardless of the caller's cwd. CONSORT_GEMINI_STDERR: file to append the Gemini CLI's stderr to
(created owner-only; dropped by default).
Fixed
- Gemini cli transport reviewed diff-only. Headless
geminimade zero
tool calls in every review run (three measured), so the "repo-reading"
transport could not do the caller sweeps the rule packs ask for. The prompt
now orders it to read every touched file and sweep callers, consumers,
schedulers and docs of every removed symbol before answering; runs after the
fix made 25–34 tool calls. - Gemini cli transport
@expansion. The CLI's@fileexpander
((?<!\\)@+ path chars) read a unified diff's@@hunk headers as file
references and injected unrelated repo files into the prompt. Every@in
the prompt is now backslash-escaped, the CLI's own escape, which headless
mode passes through as literal text. - Gemini api probe false negative.
consort_gemini_probecapped output at
16 tokens, which a thinking model spends on thoughts (finishReason: MAX_TOKENS, empty text), so a live backend read as dead. Cap raised to 256
and any candidate with afinishReasonnow counts as alive — the probe
proves reachability, auth and quota, nothing more.
consort 0.6.0 — Gemini backend
Adds a first-class Gemini (Vertex AI) backend as a cross-vendor peer to Codex, chosen at will with CONSORT_BACKEND=codex|gemini. Both are non-Anthropic, so either satisfies the cross-vendor axis when the principal is Claude.
Added
scripts/consort-backend.sh— backend dispatcher (consort_impl_call/consort_backend/consort_impl_model/consort_impl_probe).codex-backend.shandgemini-backend.share peer modules; the caller scripts source the dispatcher. Default (Codex) behavior unchanged.scripts/gemini-backend.sh— reaches Gemini on Vertex AI via ADC (gcloud/WIF), no API key. Full parity with Codex: reviewer and implementer (workspace-write edits files). Two transports (CONSORT_GEMINI_TRANSPORT=cli|api): thegeminiCLI (@google/gemini-cli) run in the workdir for repo-aware review and edits, and a stdlibgenerateContentcompanion as a diff-only, read-only CI fallback. Plus a liveness probe.- Config:
CONSORT_GEMINI_MODEL(defaultgemini-3.1-pro-preview),CONSORT_GEMINI_LOCATION(global, or a region for data residency),CONSORT_GCP_PROJECT.
Changed
consort-review.sh,consort-consult.sh,consort-delegate.shnow honorCONSORT_BACKENDvia the dispatcher.
Self-reviewed through consort's own Gemini backend (Codex was unavailable); the review's findings — a workspace-write blast-radius bug, an ARG_MAX risk, and backend/token hardening — were fixed before merge.
consort 0.5.0 — the security review tier
Security review grows real teeth: a deterministic scanner tier and a blind security side-agent join the cross-model duet. Rule-based scanners and model reviewers catch nearly disjoint defect sets — so 0.5.0 runs both, and neither substitutes for the other.
Scanner tier (model-free third voice)
scripts/consort-scan.sh+scan-to-findings.mjs— runs Trivy when on PATH (dependency CVEs, leaked secrets, IaC misconfigurations) and SonarQube when a server is reachable (SONAR_HOST_URL+SONAR_TOKEN+sonar-project.properties), including the CE-task poll and issue/hotspot fetch. Output lands in the sharedfindings.schema.json, so scanner findings merge alongside model findings.- Loud degradation, never silent: every skipped or failed scanner leg is reported on stderr; a scan that ran nothing says "this scan verified nothing" instead of passing as clean. Failed fetches and malformed scanner output are failures, not empty-but-clean results.
- Hardened by its own review gate: tokens never appear in process argv, the CE task URL is validated against the configured host,
.scannerworkstays in an invocation-owned temp dir, truncated result pages warn, Maven-style project keys with colons parse correctly. - New command:
/consort:scan— scanner tier standalone, with reachability triage.
Blind security side-agent
agents/security-reviewer— a security-only subagent the review loop spawns in parallel with the cross-vendor pass. Non-inheriting by contract: it reads the diff without the author's assumptions.- No shell by design. The agent gets Read/Grep/Glob only and receives the diff as a file written by the principal — a reviewer of possibly-hostile diffs must not be able to execute anything the diff says.
- Findings appear as their own report column and are treated as leads, verified like any cross-model finding. Same-vendor, so it complements — never satisfies — the cross-vendor requirement.
Third built-in methodology pack
rules/security-review.md, always injected into both reviewers: authorization scoping, injection at construction sites, trust-boundary shifts, secrets in motion, dangerous sinks, supply chain. Scanner findings are deterministic evidence; the model owes each one a reachability verdict. Live credentials and criticals in newly added dependencies gate regardless of diff scope.
Review reports now carry five sections: Both agree / Codex only / Claude only / Security agent / Scanners — a report missing the last two is incomplete even when the duet ran.
Full details in CHANGELOG.md.
consort v0.1.0
First release of consort — a two-vendor development lifecycle for Claude Code. The principal (any strong Claude Code session model) orchestrates, synthesizes, and verifies; Codex (gpt-5.6) implements and cross-reviews. The value is the finding only one model caught.
What's in it
- Pipeline skills —
/consort:run(interview → spec → plan → implement → review → gate),/consort:review(standalone cross-model review of the working diff),/consort:plan. - Panel + synthesis — blind independent drafts from both voices in the front phases, scored and merged with per-idea attribution.
- Delegation spine — the principal hands one task at a time to Codex with a five-part brief; verbose output stays on disk, structured results come back.
- Pluggable Codex backend — auto-uses the official Codex Claude Code plugin's companion runtime when installed, falls back to the raw
codex execCLI. Force either withCONSORT_CODEX_BACKEND=plugin|exec. - State on disk — every phase resumes from
.consort/; the session dying loses nothing. - Shared JSON schemas (spec / findings / task-result) so two vendors are machine-comparable; MIT licensed.
Requirements
Claude Code, plus either the Codex plugin or a local codex CLI (ChatGPT or API auth). Swap the implementer model via CONSORT_IMPL_MODEL.