From 7d86c6c36aff419f5b392291801541dd1bb3e58b Mon Sep 17 00:00:00 2001 From: ernestprovo23 Date: Sat, 18 Jul 2026 23:39:47 -0400 Subject: [PATCH] release: v1.2.0 --- .github/workflows/release.yml | 17 +++--- CHANGELOG.md | 36 +++++++++--- DOCUMENTATION_INDEX.md | 17 +++--- README.md | 23 ++++---- RELEASING.md | 101 +++++++++++++------------------- SECURITY.md | 2 +- SYSTEM_CONTEXT_DIAGRAM.md | 10 ++-- docs/PRODUCT_DESIGN_DOCUMENT.md | 44 +++++++------- pyproject.toml | 10 ++-- src/conclave/__init__.py | 2 +- src/conclave/cli.py | 6 +- tests/test_cli.py | 7 +++ 12 files changed, 143 insertions(+), 132 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3390528..6f56804 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,17 +1,17 @@ # ============================================================================= -# Release & publish workflow (conclave v1.0 — distribution + release-eng) +# Release & publish workflow (conclave — distribution + release engineering) # -# !!! INERT UNTIL CONFIGURED — SAFE TO MERGE !!! +# !!! RELEASE-TRIGGERED; FAILS CLOSED ON PUBLISHER MISMATCH !!! # # This workflow does NOTHING until BOTH of the following are true: # (a) a GitHub *Release* is published (a pushed git tag ALONE does not trigger # it — the trigger is `release: published`, the explicit human gesture), AND -# (b) the `conclave-cli` PyPI project + its OIDC Trusted Publisher are configured -# by the owner (see RELEASING.md "One-time PyPI setup"). +# (b) the `conclave-cli` PyPI project trusts this organization repository and +# workflow through OIDC (see RELEASING.md "Trusted Publisher prerequisite"). # # Until (a) AND (b) hold, merging this file changes nothing at runtime: no tag is -# cut here, no version is bumped here, nothing is published here. Cutting v1.0.0 is -# then a single tag + GitHub Release away (the full runbook is in RELEASING.md). +# cut here, no version is bumped here, and nothing is published here. The full +# operator runbook is in RELEASING.md. # # What it does WHEN a Release is published: # build — builds sdist + wheel with `python -m build`, uploads them as @@ -89,7 +89,8 @@ jobs: # No API token, no stored secret: pypa/gh-action-pypi-publish mints a short-lived # OIDC token from this job's `id-token: write` and exchanges it for a PyPI upload # token. PyPI must have a Trusted Publisher (pending or active) for `conclave-cli` - # pointing at owner=ernestprovo23, repo=conclave, workflow=release.yml — see + # pointing at owner=DataScience-EngineeringExperts, repo=conclave, + # workflow=release.yml — see # RELEASING.md. Until that exists, this job fails CLOSED (publish denied); it # never falls back to anything insecure. # @@ -154,7 +155,7 @@ jobs: # Self-check: verify what we just signed against THIS workflow's own # identity so a broken run never publishes a bad bundle. verify: true - verify-cert-identity: "https://github.com/ernestprovo23/conclave/.github/workflows/release.yml@${{ github.ref }}" + verify-cert-identity: "https://github.com/DataScience-EngineeringExperts/conclave/.github/workflows/release.yml@${{ github.ref }}" verify-oidc-issuer: "https://token.actions.githubusercontent.com" # release-signing-artifacts defaults to true: on a release event the # .sigstore bundles are attached to the Release assets automatically. diff --git a/CHANGELOG.md b/CHANGELOG.md index c7167f9..c332f9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.2.0] - 2026-07-18 + ### Added -- **Elite Decision Protocol (unreleased).** A quality-first `elite` mode for consequential +- **Elite Decision Protocol.** A quality-first `elite` mode for consequential decisions: independent member answers → concurrent council-wide claim audits → concurrent member revisions → the existing synthesis and canonical execution-traceable verdict. It is available - through `--mode elite`, `Council.elite()`, and `elite_sync()` in source; no version, tag, or - package publication is claimed here. + through `--mode elite`, `Council.elite()`, and `elite_sync()`. +- **Constrained-choice vote mode.** `--mode vote --choices "A,B,C"`, `Council.vote()`, + and `vote_sync()` collect a fixed labelled ballot and return a plurality winner or split. + This is complementary to verdict `provider_votes`, which cluster free-form stances. - **Fixed three-success phase gate.** Each of Elite's `initial`, `critique`, and `revision` phases requires three successful responders. Larger councils may survive partial failures while three remain. A failed gate stops later calls and returns an incomplete result with a @@ -43,6 +47,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **OpenAI strict verdict extraction.** The extraction schema now requires every declared + object property, excludes engine-computed per-conflict consensus and optional vote confidence, + and represents semantic optionals with honest empty values. Frozen replay fixtures recursively + validate recorded verdict payloads against the exact extraction contract. + - **`ModelHarnessManifest` now rides on *every* mode's result — a true invariant.** The auditable manifest was documented as first-class on every `CouncilResult`, but `debate`, `adversarial`, and `vote` built their result directly in `modes.py` and @@ -58,10 +67,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `synthesize`/`ask` only and is intentionally not layered onto `adversarial` (which already emits a judge verdict) — see PDD §4a. +### Changed + +- **Organization release identity.** Package project URLs, release signing identity, security + advisory link, and operator runbook now target + `DataScience-EngineeringExperts/conclave`. PyPI Trusted Publishing must trust that exact + organization repository and `release.yml`; publishing continues to fail closed on mismatch. +- **Complete CLI mode help.** `conclave ask --help` now describes all six published modes, + and the module overview includes the `eval` command. + ### Documentation - Added Elite CLI/library examples and reconciled the PDD, system diagram, README, and index - around its unreleased status, fixed gate, partial-failure behavior, phased receipts, and + around its release status, fixed gate, partial-failure behavior, phased receipts, and latency/cost tradeoff. - Reconciled `README.md`, `SYSTEM_CONTEXT_DIAGRAM.md`, and the PDD so the manifest-on-every-result claim is now accurate, and documented the constrained-choice @@ -124,7 +142,7 @@ reproducible arithmetic over the model's clustering — never an LLM-emitted fig - **`vote` (council issue #3):** at 1.1.0 the verdict's `provider_votes` + `consensus_label`/`consensus_score` were considered to subsume it. **Superseded post-1.1.0** - — a real constrained-choice `vote` mode later shipped (CAC-09 / #3; see `[Unreleased]`), + — a real constrained-choice `vote` mode later shipped (CAC-09 / #3; see `[1.2.0]`), complementary to `provider_votes`: a fixed ballot vs. clustered free-form stances. ## [1.0.0] - 2026-06-14 @@ -199,6 +217,8 @@ on top of v0.3.0. exit-code contract and httpx client lifecycle hardening; transport/CLI/logging test backfill; first public release with community files. -[1.1.0]: https://github.com/ernestprovo23/conclave/compare/v1.0.0...HEAD -[1.0.0]: https://github.com/ernestprovo23/conclave/compare/v0.3.0...v1.0.0 -[0.3.0]: https://github.com/ernestprovo23/conclave/releases/tag/v0.3.0 +[Unreleased]: https://github.com/DataScience-EngineeringExperts/conclave/compare/v1.2.0...HEAD +[1.2.0]: https://github.com/DataScience-EngineeringExperts/conclave/compare/v1.1.0...v1.2.0 +[1.1.0]: https://github.com/DataScience-EngineeringExperts/conclave/compare/v1.0.0...v1.1.0 +[1.0.0]: https://github.com/DataScience-EngineeringExperts/conclave/compare/v0.3.0...v1.0.0 +[0.3.0]: https://github.com/DataScience-EngineeringExperts/conclave/releases/tag/v0.3.0 diff --git a/DOCUMENTATION_INDEX.md b/DOCUMENTATION_INDEX.md index 5b0adf2..86a5e54 100644 --- a/DOCUMENTATION_INDEX.md +++ b/DOCUMENTATION_INDEX.md @@ -6,7 +6,7 @@ context diagram, and this index linking everything together. The Product Design the canonical authority spec on top of those. - **Repo:** `/Users/ernestprovo/dev/conclave/` -- **Version:** 1.1.0 · **License:** MIT +- **Version:** 1.2.0 · **License:** MIT - **Last updated:** 2026-07-18 --- @@ -15,7 +15,7 @@ the canonical authority spec on top of those. | # | Doc | Path | Purpose | |---|-----|------|---------| -| 1 | **README** (project overview) | [`README.md`](README.md) | Install, BYO-keys, five released-mode CLI/library quickstart, source-only Elite, and the gated paid exploratory eval lane. | +| 1 | **README** (project overview) | [`README.md`](README.md) | Install, BYO-keys, six-mode CLI/library quickstart including Elite, and the gated paid exploratory eval lane. | | 2 | **System Context Diagram** | [`SYSTEM_CONTEXT_DIAGRAM.md`](SYSTEM_CONTEXT_DIAGRAM.md) | Mermaid context for the provider highway, released/source modes, verdict pipeline, eval live guard, manifest receipts, and mcp-warden boundary. | | 3 | **Documentation Index** | [`DOCUMENTATION_INDEX.md`](DOCUMENTATION_INDEX.md) | This file. Master map of all docs + source layout. | @@ -23,7 +23,7 @@ the canonical authority spec on top of those. | Doc | Path | Purpose | |-----|------|---------| -| **Product Design Document** | [`docs/PRODUCT_DESIGN_DOCUMENT.md`](docs/PRODUCT_DESIGN_DOCUMENT.md) | **Canonical** product spec: released/source modes, the v1.1 execution-traceable verdict, manifest, paid exploratory evaluation boundary, architecture, and roadmap. **When docs disagree, the PDD wins.** | +| **Product Design Document** | [`docs/PRODUCT_DESIGN_DOCUMENT.md`](docs/PRODUCT_DESIGN_DOCUMENT.md) | **Canonical** product spec: six released modes, the v1.1 execution-traceable verdict, v1.2 Elite and evaluation guard, manifest, architecture, and roadmap. **When docs disagree, the PDD wins.** | ## Product plans @@ -46,9 +46,9 @@ Package root: `src/conclave/` (installed as the `conclave` package; console scri | Module | Path | Responsibility | |--------|------|----------------| -| Package API | [`src/conclave/__init__.py`](src/conclave/__init__.py) | Public exports include `Council`, result types, verdict/manifest surface, and unreleased `EliteResult` plus Elite protocol constants. | -| Council | [`src/conclave/council.py`](src/conclave/council.py) | Five released async/sync modes plus source-only Elite; Elite keeps protocol completion separate from `ready`/`not_ready`/`indeterminate` decision readiness. | -| Modes | [`src/conclave/modes.py`](src/conclave/modes.py) | Debate, adversarial, vote, and unreleased Elite orchestration; Elite gates initial/claim-audit/revision at three successes. | +| Package API | [`src/conclave/__init__.py`](src/conclave/__init__.py) | Public exports include `Council`, result types, verdict/manifest surface, `EliteResult`, and Elite protocol constants. | +| Council | [`src/conclave/council.py`](src/conclave/council.py) | Six released async/sync modes; Elite keeps protocol completion separate from `ready`/`not_ready`/`indeterminate` decision readiness. | +| Modes | [`src/conclave/modes.py`](src/conclave/modes.py) | Debate, adversarial, vote, and Elite orchestration; Elite gates initial/claim-audit/revision at three successes. | | Verdict types | [`src/conclave/verdict.py`](src/conclave/verdict.py) | Public verdict/member Pydantic types (`CouncilVerdict`, `CouncilPosition`, `CouncilConflict`, `ProviderVote`, `MinorityReport`) + the LCD JSON Schemas (`verdict_json_schema`/`member_answer_json_schema`/`verdict_extraction_json_schema`) usable across all three native structured-output surfaces; `VERDICT_SCHEMA_VERSION`/`VERDICT_EXTRACTION_PROMPT_VERSION`. | | Agreement | [`src/conclave/agreement.py`](src/conclave/agreement.py) | Deterministic consensus: `consensus_score` (`position_cluster_ratio_v1` — largest cluster / positioned members; `None` for N<2) + `consensus_label` buckets. Pure arithmetic, no `difflib`, never LLM-emitted. | | Verdict synthesis | [`src/conclave/verdict_synthesis.py`](src/conclave/verdict_synthesis.py) | `extract_verdict` engine: one initial extraction call and at most one repair, native `output_contract` enforcement + prompt-level fallback, validate → repair-once → graceful `verdict=None`; the three verdict-absent reasons; provenance on every return path. | @@ -65,7 +65,7 @@ Package root: `src/conclave/` (installed as the `conclave` package; console scri | Registry | [`src/conclave/registry.py`](src/conclave/registry.py) | Friendly-name → model-id defaults; provider → env-var mapping; key **presence** logic (never values). | | Config | [`src/conclave/config.py`](src/conclave/config.py) | Loads/merges `~/.conclave/config.yml` over defaults; resolves model ids and named/CSV councils; parses the `endpoints:` section (custom OpenAI-compatible providers). | | Models | [`src/conclave/models.py`](src/conclave/models.py) | Stable Pydantic contract, including `EliteResult` phase artifacts and backward-compatible `CouncilResult.elite`. | -| CLI | [`src/conclave/cli.py`](src/conclave/cli.py) | Five released `conclave ask` modes plus source-only Elite and `providers`; Elite exits 1 unless decision readiness is `ready`, emits full JSON before failure, and rejects streaming. | +| CLI | [`src/conclave/cli.py`](src/conclave/cli.py) | Six released `conclave ask` modes plus `providers`; Elite exits 1 unless decision readiness is `ready`, emits full JSON before failure, and rejects streaming. | | Experimental evals | [`src/conclave/evals/`](src/conclave/evals/) | Versioned DSE-708 planning, strict replay, failure-inclusive running, blinding, scoring, and exploratory reports; no product-quality claim. | | Eval CLI | [`src/conclave/eval_cli.py`](src/conclave/eval_cli.py) | Offline `plan/run/blind/report` plus `eval live`; paid execution requires `--execute`, exact USD 10.00 approval, and an owner-only `--checkpoint-seal-key-file`. | | Logging | [`src/conclave/logging.py`](src/conclave/logging.py) | Logger factory; stderr; verbosity via `CONCLAVE_LOG_LEVEL` (default `WARNING`). | @@ -97,7 +97,7 @@ Run: `pytest` (config in `pyproject.toml`, `asyncio_mode = "auto"`). |------|------|---------| | Packaging | [`pyproject.toml`](pyproject.toml) | hatchling build, deps (httpx, pydantic, rich, typer, pyyaml — no LLM SDK), dev extras, console script, pytest config. License: MIT. **PyPI distribution name `conclave-cli`** (the name `conclave` is an unrelated project); command + import stay `conclave`. | | Release runbook | [`RELEASING.md`](RELEASING.md) | Operator runbook: one-time PyPI OIDC Trusted-Publisher setup for `conclave-cli`, cut-a-release checklist (bump→tag→publish Release), post-release verification (Sigstore bundle, PEP 740 attestations), rollback/yank. | -| Changelog | [`CHANGELOG.md`](CHANGELOG.md) | Keep-a-Changelog history. `[Unreleased]` includes Elite and does not claim a version or publication. | +| Changelog | [`CHANGELOG.md`](CHANGELOG.md) | Keep-a-Changelog history through v1.2.0. | | Dev lockfile | [`requirements-dev.lock`](requirements-dev.lock) | Hash-pinned dev + runtime tree for reproducible installs/CI. Regenerate via `uv pip compile --universal --generate-hashes --python-version 3.11 --extra dev pyproject.toml -o requirements-dev.lock`. | | License | [`LICENSE`](LICENSE) | MIT License. Copyright (c) 2026 Ernest Provo. Matches the `pyproject.toml` license field. | | Security policy | [`SECURITY.md`](SECURITY.md) | BYO-keys vulnerability reporting policy: how to report, scope, and the key-handling guarantees consumers can rely on. | @@ -116,6 +116,7 @@ Run: `pytest` (config in `pyproject.toml`, `asyncio_mode = "auto"`). | Date | Change | |------|--------| +| 2026-07-18 | Released v1.2.0: Elite, constrained-choice vote, every-mode manifest hardening, the capped paid-exploratory evaluation runner, strict OpenAI-compatible verdict extraction, and org-repository release identities. The live lane remains correctness-only; no quality or efficiency claim. | | 2026-07-18 | Implemented the sequential paid exploratory runner: default dry-run, exact USD 10.00 execute approval, reservation-before-call, one in-flight call, and no-repeat resume. The 24-task fixture remains offline/open-book and is not the paid smoke corpus; the smoke is correctness-only and not decision eligible. | | 2026-07-17 | Added the experimental DSE-708 offline evaluation substrate and `conclave eval` artifact workflow; no live study or quality claim. | | 2026-07-17 | Reframed the product roadmap around empirically proven decision quality: narrowed current claims to execution traceability, identified answer IDs as internal provenance rather than external evidence, gated Elite merge on H0 correctness, and added H1-H4 evidence, buyer, and outcome gates. | diff --git a/README.md b/README.md index 4f17540..a328eea 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ It is **library-first** (the CLI is a thin shell over the same `Council` you imp returns **structured results** (per-model latency, token usage, and error capture), and is **partial-failure resilient** — one provider erroring never aborts the run. Keys are **bring-your-own**, referenced by environment-variable *name* only — never stored or -logged. Published v1.1 ships five modes: **synthesize** (merge answers into one), **raw** (no merge), +logged. Published v1.2 ships six modes: **synthesize** (merge answers into one), **raw** (no merge), **debate** (multi-round, members revise after seeing peers' anonymized answers), and -**adversarial** (propose → refute → verdict), and **vote** (fixed-choice tally). This source -branch also implements the unreleased **elite** quality-first claim-audit and revision mode. +**adversarial** (propose → refute → verdict), **vote** (fixed-choice tally), and **elite** +(quality-first independent answers → claim audits → revisions → verdict). conclave is intentionally lightweight — a small council primitive, not an agent framework. **The v1.1 wedge — the execution-traceable council.** Decision/review synthesis runs can yield @@ -25,8 +25,8 @@ execution-traceable**: a a deterministic `consensus_score` (arithmetic over the model's clustering, *never* an LLM-emitted number); and a redacted `ModelHarnessManifest` recording how the run executed and which model produced the disagreement analysis. The verdict is **default-on**, and the manifest -rides on **every** released-mode result; source-only Elite results carry it too. A constrained-choice **`vote` mode** (`--mode vote --choices ...`) also -shipped in v1.1 (CAC-09 / #3) — distinct from the verdict's `provider_votes`, which score +rides on **every** mode result, including Elite. A constrained-choice **`vote` mode** (`--mode vote --choices ...`) also +shipped in v1.2 (CAC-09 / #3) — distinct from the verdict's `provider_votes`, which score free-form agreement rather than tally a fixed ballot. See the canonical spec and design docs: @@ -126,7 +126,7 @@ conclave ask "Defend event sourcing for this ledger." \ conclave ask "Which datastore for this workload?" \ -c grok,gemini,claude --mode vote --choices "Postgres,DynamoDB,MongoDB" -# Elite (unreleased): independent answers -> claim audits -> revisions -> verdict +# Elite: independent answers -> claim audits -> revisions -> verdict conclave ask "Should we adopt a service mesh for 8 services?" \ -c grok,gemini,claude --mode elite @@ -134,17 +134,14 @@ conclave ask "Should we adopt a service mesh for 8 services?" \ conclave ask "..." -c grok,perplexity --mode debate --json ``` -Published v1.1 mode flags are `--mode synthesize|raw|debate|adversarial|vote`; this source -branch additionally exposes `--mode elite`. `--rounds N` +Published v1.2 mode flags are `--mode synthesize|raw|debate|adversarial|vote|elite`. `--rounds N` (default 2) is the *maximum* round count for `debate`; `--converge-threshold FLOAT` (or `--converge`/`--no-converge`) optionally stops a debate early once answers stabilize round-over-round (off by default — `--rounds` runs in full). `--proposer NAME` (default: first member) applies to `adversarial`. `--choices "A,B,C"` (two or more) is required for `vote`. `--synthesizer/-s` overrides the synthesizer *and* the -adversarial judge and, on this source branch, Elite's final synthesizer. Every released mode's -result carries the auditable `ModelHarnessManifest`; source-only Elite results do too. Elite is -currently **unreleased**; use it from this source branch until -a future release explicitly includes it. +adversarial judge and Elite's final synthesizer. Every mode's result carries the auditable +`ModelHarnessManifest`. `--council` accepts either a comma-separated list of friendly names or the name of a council defined in your config (see below). The built-in `default` council @@ -200,7 +197,7 @@ for answer in result.answers: print("SYNTHESIS:\n", result.synthesis) ``` -### Elite Decision Protocol (unreleased) +### Elite Decision Protocol Elite is the quality-first path for consequential decisions. It trades latency and provider spend for a stronger answer: three concurrent member phases, followed by conclave's existing diff --git a/RELEASING.md b/RELEASING.md index 211b618..5c157be 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -9,7 +9,7 @@ Three names matter and they are deliberately different: | PyPI distribution name (what `pip install` uses) | `conclave-cli` | | CLI command (what users type) | `conclave` | | Import package (what you `import`) | `conclave` | -| GitHub repository | `ernestprovo23/conclave` | +| GitHub repository | `DataScience-EngineeringExperts/conclave` | Install is therefore `pip install conclave-cli`, but the command stays `conclave` and the import stays `from conclave import Council`. The PyPI name `conclave` is an @@ -18,87 +18,66 @@ distribution name is `conclave-cli`. The publish + signing automation lives in [`.github/workflows/release.yml`](.github/workflows/release.yml). That workflow is -**inert until configured**: it only fires when a GitHub *Release* is published, and -the publish job only succeeds once the one-time PyPI Trusted Publisher below exists. +triggered only when a GitHub *Release* is published. The publish job succeeds only when +PyPI trusts the exact organization repository and workflow identity below. --- -## 0. One-time PyPI setup (do this ONCE, before the first release) +## 0. PyPI Trusted Publisher prerequisite -The workflow publishes via **OIDC Trusted Publishing** — there is no API token and -no secret stored in GitHub. Instead, PyPI is told to trust releases that come from -this exact repo + workflow. Configure the publisher *before* the first release so -the very first upload is already OIDC-published. +The workflow publishes through **OIDC Trusted Publishing**; there is no API token or +stored GitHub secret. On PyPI, open **conclave-cli → Manage → Publishing** and confirm +an active publisher with these exact values: -### Recommended path — "pending publisher" (zero prior upload required) +- **Owner:** `DataScience-EngineeringExperts` +- **Repository:** `conclave` +- **Workflow:** `release.yml` +- **Environment:** blank (the workflow has no GitHub environment) -1. Log in to as the account that will own `conclave-cli`. -2. Go to **Account → Publishing** (). -3. Under **Add a new pending publisher**, fill in **exactly**: - - **PyPI Project Name**: `conclave-cli` - - **Owner**: `ernestprovo23` - - **Repository name**: `conclave` - - **Workflow name**: `release.yml` - - **Environment name**: *(leave blank — the workflow does not use a GitHub - deployment environment; if you later add one, set it here and add an - `environment:` block to the `pypi-publish` job)* -4. Save. PyPI now reserves the project name `conclave-cli` and will create it on the - first successful OIDC upload from `release.yml`. - -A "pending publisher" reserves the name and lets the FIRST release be OIDC-published -— no manual upload, no token ever. This is the clean path for conclave: there is no -prior token-publish history, so the supply chain is OIDC-only from release #1. - -### Alternative path — manual first upload, then configure - -If you would rather seed the project manually first: - -1. Build locally: `python -m build` (produces `dist/*.tar.gz` + `dist/*.whl`). -2. `twine upload dist/*` with a temporary PyPI API token (creates `conclave-cli`). -3. Then go to **Manage project → Publishing** on the new `conclave-cli` project and - add the Trusted Publisher with the same owner/repo/workflow values as above. -4. Revoke the temporary token. - -> Prefer the pending-publisher path. It avoids ever minting a long-lived token and -> keeps the entire supply chain OIDC-only from release #1. +If the repository was transferred from `ernestprovo23/conclave`, replace the old publisher +before releasing. Public provenance proves the identity used by past releases, not the current +private publisher configuration. A mismatch fails closed in the publish job; never fall back to +a token upload. --- ## 1. Cut a release -Do this on a clean checkout of `main` with all v1 PRs merged. +Do this on a clean checkout of `main` with all intended release changes merged. 1. **Update the changelog.** In [`CHANGELOG.md`](CHANGELOG.md), move the - `## [Unreleased]` entries under a new `## [1.0.0] - ` heading with + `## [Unreleased]` entries under a new `## [X.Y.Z] - ` heading with today's date. Leave a fresh empty `## [Unreleased]` section above it. 2. **Bump the version in BOTH places.** - - In [`pyproject.toml`](pyproject.toml), set `[project] version = "1.0.0"`. + - In [`pyproject.toml`](pyproject.toml), set `[project] version = "X.Y.Z"`. - In [`src/conclave/__init__.py`](src/conclave/__init__.py), set - `__version__ = "1.0.0"`. + `__version__ = "X.Y.Z"`. (The distribution name `conclave-cli` is already set — do **not** change it.) 3. **Commit.** ```bash git add CHANGELOG.md pyproject.toml src/conclave/__init__.py - git commit -m "release: v1.0.0" - git push origin main + git commit -m "release: vX.Y.Z" + git push -u origin release/X.Y.Z ``` -4. **Tag and push the tag.** (A tag alone does NOT publish anything — it only marks +4. **Open and merge the release PR.** Wait for every required CI check, merge through + branch protection, then fast-forward a clean local `main`. The tag must point to the + merged release commit, not the pre-merge branch commit. + +5. **Tag and push the tag.** (A tag alone does NOT publish anything — it only marks the commit. The Release in the next step is what triggers the workflow.) ```bash - git tag v1.0.0 - git push origin v1.0.0 + git tag vX.Y.Z + git push origin vX.Y.Z ``` -5. **Create the GitHub Release.** This is the trigger. +6. **Create the GitHub Release.** This is the trigger. ```bash - gh release create v1.0.0 \ - --title "v1.0.0" \ - --notes-file <(awk '/## \[1.0.0\]/{f=1} /## \[0\./{if(f)exit} f' CHANGELOG.md) + gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes ``` - or use the GitHub UI: **Releases → Draft a new release → choose tag `v1.0.0` → + or use the GitHub UI: **Releases → Draft a new release → choose tag `vX.Y.Z` → Publish release**. Publishing the Release fires `release.yml`, which: @@ -122,9 +101,9 @@ Do this on a clean checkout of `main` with all v1 PRs merged. python -c "import conclave; print(conclave.__version__)" ``` The install name is `conclave-cli`, the command is `conclave`, the import is - `conclave`. The `python -c` line must print `1.0.0` (there is no `--version` + `conclave`. The `python -c` line must print `X.Y.Z` (there is no `--version` flag; the running version is shown in the `conclave providers` footer). - Remember to bump `__version__` in `src/conclave/__init__.py` to `1.0.0` in the + Remember to bump `__version__` in `src/conclave/__init__.py` to `X.Y.Z` in the release commit (step 1.2) alongside `pyproject.toml`. 2. **Verify the Sigstore bundle.** On the GitHub Release page, confirm there is a @@ -133,17 +112,17 @@ Do this on a clean checkout of `main` with all v1 PRs merged. re-verify any artifact locally: ```bash pip install sigstore - sigstore verify identity dist/conclave_cli-1.0.0-py3-none-any.whl \ - --bundle conclave_cli-1.0.0-py3-none-any.whl.sigstore \ + sigstore verify identity dist/conclave_cli-X.Y.Z-py3-none-any.whl \ + --bundle conclave_cli-X.Y.Z-py3-none-any.whl.sigstore \ --cert-identity \ - "https://github.com/ernestprovo23/conclave/.github/workflows/release.yml@refs/tags/v1.0.0" \ + "https://github.com/DataScience-EngineeringExperts/conclave/.github/workflows/release.yml@refs/tags/vX.Y.Z" \ --cert-oidc-issuer "https://token.actions.githubusercontent.com" ``` (Download the `.whl` and its `.sigstore` bundle from the Release assets first.) 3. **Confirm the PyPI page.** Visit and check: - - version `1.0.0` is listed; - - the project URLs (homepage / repository) point at `ernestprovo23/conclave`; + - version `X.Y.Z` is listed; + - the project URLs point at `DataScience-EngineeringExperts/conclave`; - "Publisher" shows the Trusted Publisher (OIDC), not a token upload; - PEP 740 attestations are present (the verified-publish badge). @@ -157,8 +136,8 @@ release is broken: - **Yank** the bad version (keeps existing pins working, hides it from new installs): on → **Manage → Releases → Options → Yank**. Yanking is reversible. -- **Ship a fix-forward release** (`1.0.1`) following section 1 again. This is the - preferred remedy — never try to re-upload `1.0.0`. +- **Ship a fix-forward patch release** following section 1 again. This is the + preferred remedy — never try to re-upload the same version. - **GitHub Release**: you may delete or edit the GitHub Release and its assets freely; that does not affect what is already on PyPI. Re-running the workflow against the same version will fail the PyPI publish (duplicate filename), which is diff --git a/SECURITY.md b/SECURITY.md index 52eb210..946d860 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -167,7 +167,7 @@ preferred because it keeps the report, the fix, and the CVE in one place): 1. **GitHub Security Advisories** — go to the repository's **Security** tab and click **"Report a vulnerability"** - (). This + (). This opens a private advisory visible only to you and the maintainers. 2. **Email** — `ernest@thedataexperts.us`. Use a clear subject line such as `[conclave security]`. If you want to encrypt, say so in a first plaintext diff --git a/SYSTEM_CONTEXT_DIAGRAM.md b/SYSTEM_CONTEXT_DIAGRAM.md index f67bd5f..7384d58 100644 --- a/SYSTEM_CONTEXT_DIAGRAM.md +++ b/SYSTEM_CONTEXT_DIAGRAM.md @@ -6,7 +6,7 @@ environment-variable keys feed in, how requests reach the nine first-class provi conclave's own **provider highway** (an httpx transport + per-provider adapter registry — no LLM-SDK dependency), how the v1.1 **verdict pipeline** turns the member answers into a structured, agreement-scored, **execution-traceable** verdict plus a redacted execution manifest, and -how the implemented-but-unreleased **Elite Decision Protocol** adds gated claim audit and +how the v1.2 **Elite Decision Protocol** adds gated claim audit and revision before that verdict. It also shows where **mcp-warden** sits as a dev-time consumer. The DSE-708 edge includes offline replay plus an opt-in, capped paid exploratory path. @@ -32,7 +32,7 @@ flowchart TB lib["Library API · from conclave import Council (__init__.py)"] council["Council orchestrator
fan_out · synthesize_blocks · skip-no-key (council.py)"] modes["Deliberation modes
debate · adversarial · vote (modes.py + prompts.py)"] - elite["Elite (UNRELEASED)
initial → claim audit → revision
fixed 3-success gate each phase"] + elite["Elite (v1.2)
initial → claim audit → revision
fixed 3-success gate each phase"] evalcli["Experimental eval CLI (DSE-708)
offline replay · live dry-run
gated paid exploratory execute"] evalartifacts["Versioned eval artifacts
manifest · run · grader set + separate map
exploratory report"] liveguard["Live guard
exact USD 10.00 approval · owner-only seal key
HMAC checkpoint · one in flight · no-repeat resume"] @@ -168,14 +168,14 @@ flowchart TB (`manifest.py`) rides on **every** result — first-class, not a debug flag — recording which model + prompt version produced the clustering (provenance) and stamping `secret_safety` only after the serialized manifest is scanned clean. This is enforced as a true invariant at - the single chokepoint `Council._cached_run` → `_ensure_manifest`: all five released modes - (`synthesize`, `raw`, `debate`, `adversarial`, `vote`) plus source-only `elite` funnel through + the single chokepoint `Council._cached_run` → `_ensure_manifest`: all six modes + (`synthesize`, `raw`, `debate`, `adversarial`, `vote`, `elite`) funnel through it, so a result can never escape without a manifest — including the zero-members early return and cache hits. A verdict is *optional*: open-ended prompts, fewer than two responding members, or extraction failure leave `verdict = None` with the synthesis and member answers intact and the reason recorded on the manifest. -- **Elite is quality-first and unreleased.** Its three concurrent member phases are independent +- **Elite is quality-first and released in v1.2.** Its three concurrent member phases are independent answers, council-wide anonymized claim audits, and member revisions. Answer IDs trace outputs within the run; they are not external citations. Each phase requires three successful responders. Larger councils may lose members and continue while three remain; diff --git a/docs/PRODUCT_DESIGN_DOCUMENT.md b/docs/PRODUCT_DESIGN_DOCUMENT.md index 0305037..a79f38c 100644 --- a/docs/PRODUCT_DESIGN_DOCUMENT.md +++ b/docs/PRODUCT_DESIGN_DOCUMENT.md @@ -1,10 +1,11 @@ # conclave — Product Design Document -> **Status:** v1.1 stable (the BYO-keys multi-model council: synthesize/raw/debate/ -> adversarial/vote, 9 providers, owned httpx provider highway, key-leak hardening, streaming, +> **Status:** v1.2 stable (the BYO-keys multi-model council: synthesize/raw/debate/ +> adversarial/vote/elite, 9 providers, owned httpx provider highway, key-leak hardening, streaming, > cache). **v1.1 — the auditable council — SHIPPED:** every run now yields a structured, > agreement-scored, execution-traceable **verdict** plus a redacted execution **manifest** (see -> §4a). The quality-first **Elite Decision Protocol is implemented but unreleased**. +> §4a). **v1.2 ships the quality-first Elite Decision Protocol** and the guarded paid-exploratory +> evaluation runner; no decision-quality or efficiency claim is made from the smoke. > This is the **canonical authority document** for conclave's product scope, design, > and roadmap. When this document and any other doc disagree, this document wins. Code is the > source of truth for *current behavior*; this document marks anything not yet in code as @@ -13,7 +14,7 @@ - **Repo:** `/Users/ernestprovo/dev/conclave/` - **License:** MIT - **Author:** Data Science & Engineering Experts, Inc. (DSE) -- **Last updated:** 2026-07-17 +- **Last updated:** 2026-07-18 --- @@ -48,12 +49,12 @@ structured, scored for agreement, and execution-traceable.** Every run yields a exposing agreement, disagreement (`conflicts`), minority views (`minority_reports`), and per-provider votes (`provider_votes`); a deterministic `consensus_score` (arithmetic over the model's clustering, *never* an LLM-emitted number); and a redacted `ModelHarnessManifest` (how -the run executed + which model produced the analysis) riding on **every** released-mode result; -source-only Elite results carry it too (one chokepoint, §4a). A constrained-choice **`vote` +the run executed + which model produced the analysis) riding on **every** mode result, +including Elite (one chokepoint, §4a). A constrained-choice **`vote` mode** also **shipped** (CAC-09 / #3, `--mode vote --choices "A,B,C"` → plurality winner/split) — distinct from `provider_votes`. -The next product-quality step is **Elite** (implemented, unreleased): independent answers → council-wide answer/claim audits → member revisions → the existing synthesis and execution-traceable verdict. Elite requires three successful responders at every member phase and intentionally spends more calls and time to improve consequential decisions. +v1.2 adds **Elite**: independent answers → council-wide answer/claim audits → member revisions → the existing synthesis and execution-traceable verdict. Elite requires three successful responders at every member phase and intentionally spends more calls and time for consequential decisions. Whether it improves decision quality remains an empirical question for H1. conclave's first real use was an **adversarial design review**: a council of Grok, Gemini, Perplexity, and Claude critiquing a security-tool strategy and catching flaws a single @@ -129,8 +130,8 @@ output. The v1.1 verdict layer (§4a) sits on top of whichever mode produced the | **raw** | **BUILT (v0.1)** | Fan out and return every member's raw answer with no synthesis. Not a deliberation mode — it is "synthesize off." Exposed as `--mode raw` / `ask(..., synthesize=False)`. | | **debate** | **BUILT (v0.2)** | N rounds (`--rounds`, default 2). Round 1 is an independent fan-out; rounds 2..N show each member its peers' **anonymized** prior-round answers (`Model A/B/C`) and ask it to revise or defend. A member that errors in a round drops out of later rounds; the debate continues with survivors. The synthesizer consolidates the final round. Exposed as `--mode debate` / `Council.debate()` / `debate_sync()`. | | **adversarial** | **BUILT (v0.2)** | Structured propose → refute → verdict. A `--proposer` (default: first member) answers; the remaining members are CRITICS explicitly prompted to refute it; the synthesizer acts as JUDGE, weighing proposal vs. critiques and issuing a verdict + strengthened answer. This is the mode conclave's origin story (the security design review) exercised by hand. Exposed as `--mode adversarial` / `Council.adversarial()` / `adversarial_sync()`. | -| **vote** | **BUILT (v1.1, CAC-09 / #3)** | Constrained-choice ballot: each member sees a fixed labelled option set (`A, B, C, …`) and answers with one letter; responses are tallied to a plurality `winner` (or `split` on a tie) on `result.vote` (`VoteResult`). Exposed as `--mode vote --choices "A,B,C"` / `Council.vote()` / `vote_sync()`. Distinct from the verdict's `provider_votes`, which cluster free-form stances with evidence (§4a); `vote` tallies a fixed ballot. | -| **elite** | **IMPLEMENTED, UNRELEASED** | Quality-first decision protocol: independent fan-out → concurrent council-wide claim audits → concurrent member revisions → existing synthesis and canonical verdict. Every member phase has a fixed three-success gate. Exposed in source as `--mode elite` / `Council.elite()` / `elite_sync()`; buffered only, no streaming. | +| **vote** | **BUILT (v1.2, CAC-09 / #3)** | Constrained-choice ballot: each member sees a fixed labelled option set (`A, B, C, …`) and answers with one letter; responses are tallied to a plurality `winner` (or `split` on a tie) on `result.vote` (`VoteResult`). Exposed as `--mode vote --choices "A,B,C"` / `Council.vote()` / `vote_sync()`. Distinct from the verdict's `provider_votes`, which cluster free-form stances with evidence (§4a); `vote` tallies a fixed ballot. | +| **elite** | **BUILT (v1.2)** | Quality-first decision protocol: independent fan-out → concurrent council-wide claim audits → concurrent member revisions → existing synthesis and canonical verdict. Every member phase has a fixed three-success gate. Exposed as `--mode elite` / `Council.elite()` / `elite_sync()`; buffered only, no streaming. | **Mode algorithms (as built).** The step-by-step "as built" prose for synthesize / raw / debate / adversarial / vote (fan-out + partial-results, peer anonymization + drop-out, proposer → @@ -254,11 +255,11 @@ verdict appears identically in the buffered result and the streaming `done` even `secret_safety` stamp is re-run over the final content. **Cost:** default-on adds one initial synthesizer call and at most one schema-repair retry; the opt-out exists for cost-sensitive callers. -**Verdict scope across modes (deliberate).** Posture: *manifest everywhere, clustering verdict only where unambiguously additive.* The manifest rides on all five released modes and on source-only Elite. The clustering verdict runs on `synthesize`/`ask` and on **completed Elite runs after synthesis of the revised answers**; it is deferred on `debate`/`vote` and intentionally not layered onto `adversarial`, which already emits a judge verdict. An incomplete Elite run has no synthesis or verdict. +**Verdict scope across modes (deliberate).** Posture: *manifest everywhere, clustering verdict only where unambiguously additive.* The manifest rides on all six modes. The clustering verdict runs on `synthesize`/`ask` and on **completed Elite runs after synthesis of the revised answers**; it is deferred on `debate`/`vote` and intentionally not layered onto `adversarial`, which already emits a judge verdict. An incomplete Elite run has no synthesis or verdict. ### ModelHarnessManifest — first-class, secret-free The `ModelHarnessManifest` (`manifest.py`) rides on every `CouncilResult` — *not* behind a debug -flag, and now a **true invariant** enforced at one chokepoint: all five released modes plus source-only Elite funnel through +flag, and now a **true invariant** enforced at one chokepoint: all six modes funnel through `Council._cached_run` → `_ensure_manifest`, which attaches the manifest on every returned result — including `debate`/`adversarial`/`vote` (built in `modes.py`), the zero-members early return, and cache hits (synthesize/raw builds its own richer one earlier). Pinned by @@ -327,12 +328,12 @@ consumers. The end-to-end flow — `CLI/Library → Council → call_model → a | Module | Responsibility | |--------|----------------| -| `council.py` | `Council` — primary importable entry point. Resolves names, partitions members, and exposes two reusable primitives: `fan_out` (the single concurrent + partial-failure call loop) and `synthesize_blocks` (the single synthesizer/judge call path). Hosts the async/sync APIs for five released modes plus unreleased `elite`/`elite_sync`. Every source mode funnels through `_cached_run`; completed Elite runs synthesize revisions and then use `_apply_verdict`. | +| `council.py` | `Council` — primary importable entry point. Resolves names, partitions members, and exposes two reusable primitives: `fan_out` (the single concurrent + partial-failure call loop) and `synthesize_blocks` (the single synthesizer/judge call path). Hosts the async/sync APIs for all six modes. Every mode funnels through `_cached_run`; completed Elite runs synthesize revisions and then use `_apply_verdict`. | | `verdict.py` | Public verdict/member Pydantic types (`CouncilVerdict`, `CouncilPosition`, `CouncilConflict`, `ProviderVote`, `MinorityReport`) + the LCD JSON Schemas (`verdict_json_schema`/`member_answer_json_schema`/`verdict_extraction_json_schema`) usable across all three native structured-output surfaces; `VERDICT_SCHEMA_VERSION`. | | `agreement.py` | Deterministic consensus: `consensus_score` (`position_cluster_ratio_v1` — largest cluster / positioned members; `None` for N<2) + `consensus_label` buckets. Pure arithmetic, no `difflib`, never LLM-emitted. | | `verdict_synthesis.py` | `extract_verdict` engine: one initial extraction call and at most one repair (clusters stances, never emits a number), native `output_contract` enforcement + prompt-level fallback, validate → repair-once → graceful `verdict=None`; the three verdict-absent reasons; provenance on every return path. | | `manifest.py` | `ModelHarnessManifest` (first-class on every result), phased `ProviderExecutionReceipt`/`ProviderSkip`/`VerdictExtraction`, and `scan_for_secret_material()` → `secret_safety` stamp. No key values; unknown `estimated_cost` remains `None`. | -| `modes.py` | Deliberation orchestration: `run_debate`, `run_adversarial`, `run_vote`, and unreleased `run_elite` (three gated member phases). Built on `Council.fan_out` + `synthesize_blocks`. | +| `modes.py` | Deliberation orchestration: `run_debate`, `run_adversarial`, `run_vote`, and `run_elite` (three gated member phases). Built on `Council.fan_out` + `synthesize_blocks`. | | `prompts.py` | Role/template strings for debate, adversarial, vote, and Elite claim-audit/revision prompts. Elite panel text uses stable Model A/B/C aliases and answer ids, never provider identities. | | `providers.py` | `call_model` (+ `call_model_stream`) — the single async call path: resolve adapter, read key *by name at call time*, call adapter+transport (with an optional `output_contract`), parse, capture latency/usage/redacted-error into a `ModelAnswer`; never raises for provider-side failures. | | `transport.py` | The single async network boundary: `post_json` (buffered) + `stream_sse` (issue #7) — the only two httpx call sites in the highway. | @@ -382,10 +383,13 @@ Condensed history (v0.x mode-detail archived per §4, per-release changelog in ` `conflicts`/`provider_votes`/`minority_reports` + first-class `manifest`; deterministic `position_cluster_ratio_v1` consensus; native + fallback structured output across OpenAI/Anthropic/Gemini; the verdict-optional rule; verdict default-on with - `Council(extract_verdict=False)` opt-out; the auditable `manifest` made a true - every-mode invariant (one chokepoint). A constrained-choice **`vote` mode** shipped - (CAC-09 / #3) — `--mode vote --choices` — distinct from the verdict's `provider_votes`. - Full detail in §4a. + `Council(extract_verdict=False)` opt-out. Full detail in §4a. +- **v1.2 (quality-first protocol + evaluation guard):** released Elite with three-success + phase gates, execution receipts, and readiness tri-state; shipped constrained-choice `vote`; + made the auditable manifest a true every-mode invariant at one chokepoint; and added the + sequential paid-exploratory evaluation runner with dry-run default, exact USD 10 cap, + authenticated checkpoints, bounded price estimates, and replay. The live smoke establishes + operational correctness only. --- @@ -410,8 +414,8 @@ council shipped in v1.1** (§4a — the wedge). The revised thesis is a **source-grounded, execution-traceable decision record with empirically proven quality**. Current answer IDs identify model outputs, not external evidence; -source-auditable language is therefore too broad until source grounding ships. Elite remains -implemented but unreleased in source. +source-auditable language is therefore too broad until source grounding ships. Elite shipped in +v1.2, but its decision-quality advantage remains unproven pending H1 evidence. H1 also includes an opt-in live runner that is **paid exploratory only**. Dry-run is the default; paid execution requires `--execute`, exact `--approve-spend-usd 10.00`, and an owner-only @@ -486,7 +490,7 @@ the execution-traceable verdict, the owned provider layer, the no-aggregator pos **Open:** none currently. -2. ~~**`vote` answer schema.**~~ **RESOLVED in v1.1** by *two* complementary deliveries: (a) a +2. ~~**`vote` answer schema.**~~ **RESOLVED in v1.2** by *two* complementary deliveries: (a) a real constrained-choice **`vote` mode** (CAC-09 / #3) — fixed labelled ballot, plurality winner/split on `result.vote`; and (b) the verdict's structured `provider_votes`, which cluster free-form stances with evidence (LCD JSON Schemas + native structured output, §4a). diff --git a/pyproject.toml b/pyproject.toml index b6b66d0..3ab2ee2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "hatchling.build" # `conclave` — only the published *distribution* name differs. So users # `pip install conclave-cli`, then run `conclave ...` / `from conclave import Council`. name = "conclave-cli" -version = "1.1.0" +version = "1.2.0" description = "Bring-your-own-keys multi-model council CLI + library. Fan a prompt to N foundation models concurrently and merge their answers." readme = "README.md" requires-python = ">=3.11" @@ -26,10 +26,10 @@ dependencies = [ ] [project.urls] -Homepage = "https://github.com/ernestprovo23/conclave" -Repository = "https://github.com/ernestprovo23/conclave" -Issues = "https://github.com/ernestprovo23/conclave/issues" -Changelog = "https://github.com/ernestprovo23/conclave/blob/main/CHANGELOG.md" +Homepage = "https://github.com/DataScience-EngineeringExperts/conclave" +Repository = "https://github.com/DataScience-EngineeringExperts/conclave" +Issues = "https://github.com/DataScience-EngineeringExperts/conclave/issues" +Changelog = "https://github.com/DataScience-EngineeringExperts/conclave/blob/main/CHANGELOG.md" [project.optional-dependencies] dev = [ diff --git a/src/conclave/__init__.py b/src/conclave/__init__.py index 97922bb..6644327 100644 --- a/src/conclave/__init__.py +++ b/src/conclave/__init__.py @@ -78,7 +78,7 @@ extract_verdict, ) -__version__ = "1.1.0" +__version__ = "1.2.0" __all__ = [ "Council", diff --git a/src/conclave/cli.py b/src/conclave/cli.py index 74fa073..137e5e0 100644 --- a/src/conclave/cli.py +++ b/src/conclave/cli.py @@ -1,12 +1,14 @@ """conclave command-line interface. -Two commands: +Three commands: * ``conclave ask "" --council grok,gemini,claude --mode synthesize`` Modes: ``synthesize`` (default), ``raw``, ``debate`` (``--rounds N``), ``adversarial`` (``--proposer NAME``), ``vote``, and ``elite``. * ``conclave providers`` -- show which providers currently have a key (without ever printing key values). +* ``conclave eval`` -- plan or replay offline studies and run the separately gated + paid-exploratory live lane. """ from __future__ import annotations @@ -510,7 +512,7 @@ def ask( ), ), ) -> None: - """Fan PROMPT out to a council and synthesize, debate, or adversarially review. + """Run one of six council modes over PROMPT: synthesize, raw, debate, adversarial, vote, or elite. Exit codes: diff --git a/tests/test_cli.py b/tests/test_cli.py index 2984dfc..ff3fa02 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -298,6 +298,13 @@ def test_ask_help_lists_elite_mode(): assert "elite" in result.output +def test_ask_help_describes_all_six_modes(): + """The command summary must not describe only the original mode subset.""" + result = runner.invoke(cli.app, ["ask", "--help"]) + assert result.exit_code == 0 + assert "Run one of six council modes" in result.output + + def test_unresolved_council_exits_two(patch_cli_config): """Usage error (empty council selector resolves to zero members) -> exit 2.""" result = runner.invoke(cli.app, ["ask", "hello", "--council", " , "])