From 90f99588aef4fecfe96fca27d1f59b6a1f6f3f59 Mon Sep 17 00:00:00 2001 From: quantamixsol Date: Sat, 10 Oct 2026 10:44:11 +0200 Subject: [PATCH 1/4] fix(CR-LIC-0851): edition guard no longer recommends unpublished graqle-studio (0.85.1 surgical patch) Guard-only hunk of fb87bf5f applied to the v0.85.0 base: the Community degrade message points at graqle.com/pricing instead of 'pip install graqle-studio' (not on PyPI). Excludes the webhook/billing/register changes in fb87bf5f and the held dd704ea2. State: WORKING - version bump, CHANGELOG and wheel check pending. Co-Authored-By: Claude Sonnet 5.5 --- graqle/cli/_edition_guard.py | 24 ++++++++++++++---------- tests/test_cli/test_edition_guard.py | 9 +++++---- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/graqle/cli/_edition_guard.py b/graqle/cli/_edition_guard.py index b74316b2..1fc4317b 100644 --- a/graqle/cli/_edition_guard.py +++ b/graqle/cli/_edition_guard.py @@ -72,18 +72,22 @@ def _is_proprietary_absence(exc: ModuleNotFoundError) -> str | None: return None +#: Live page that explains the editions (verified 200 on 2026-09-27). +PLANS_URL = "https://graqle.com/pricing" + + def _degrade_message(missing_module: str, command_label: str) -> str: - """Build the clean, actionable degrade message (no dead URLs — AUD-029).""" - dist = _distribution_for(missing_module) - # TODO(WS-H): append the editions overview URL (e.g. https://graqle.com/editions) - # once the site page ships. Until then, give only the actionable install path — - # never a dead link (honesty / journey-trace rule). + """Build the clean, actionable degrade message (no dead ends — AUD-029). + + CR-LIC-0851: this used to tell users to install a ``graqle-studio`` + distribution. That package is not on PyPI, so the only "actionable" step + was a dead end. The commercial + backends are hosted, not installed; point at the live plans page instead. + """ return ( - f"[bold yellow]'{command_label}' requires a GraQle commercial edition.[/bold yellow]\n\n" - f"This is part of GraQle Studio/Enterprise and is not included in the free " - f"Community edition.\n\n" - f"To enable it, install the commercial package:\n" - f" [cyan]pip install {dist}[/cyan]\n\n" + f"[bold yellow]'{command_label}' is part of the hosted GraQle service " + f"and is not included in the free Community edition.[/bold yellow]\n\n" + f"See plans: [cyan]{PLANS_URL}[/cyan] · Enterprise: sales@graqle.com\n\n" f"The free Community edition includes reasoning, governance, tamper-evidence, " f"the offline verifier, and local anchoring — all unchanged." ) diff --git a/tests/test_cli/test_edition_guard.py b/tests/test_cli/test_edition_guard.py index d9a5db35..316a8b09 100644 --- a/tests/test_cli/test_edition_guard.py +++ b/tests/test_cli/test_edition_guard.py @@ -82,13 +82,14 @@ def test_is_proprietary_absence_none_name(): # ---- _degrade_message --------------------------------------------------------- -def test_degrade_message_has_install_path_no_dead_url(): +def test_degrade_message_has_no_dead_end(): msg = _degrade_message("graqle.cloud.credentials", "graq cloud status") - assert "pip install graqle-studio" in msg assert "graq cloud status" in msg - # AUD-029: must NOT hardcode the not-yet-existent editions URL + # CR-LIC-0851: graqle-studio is not on PyPI — never tell users to install it. + assert "pip install" not in msg + # AUD-029: only live pages; /editions does not exist. assert "graqle.com/editions" not in msg - assert "http" not in msg + assert "https://graqle.com/pricing" in msg # ---- requires_package decorator ---------------------------------------------- From 77f21de764bb21ee49fecec78439eca262a2adf6 Mon Sep 17 00:00:00 2001 From: quantamixsol Date: Sat, 10 Oct 2026 10:55:55 +0200 Subject: [PATCH 2/4] chore(release): 0.85.1 version, manifests, changelog (pyproject.toml pending G4 approval) State: WORKING. pyproject.toml still 0.85.0 - blocked by G4_PROTECTED_PATH, needs owner approval. Co-Authored-By: Claude Sonnet 5.5 --- .agents/plugins/marketplace.json | 2 +- .claude-plugin/marketplace.json | 2 +- CHANGELOG.md | 16 ++++++++++++++++ graqle/__version__.py | 2 +- .../graqle/.claude-plugin/plugin.json | 2 +- plugins/codex/graqle/.codex-plugin/plugin.json | 2 +- server.json | 4 ++-- 7 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index b9ac884c..45e473cf 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -13,7 +13,7 @@ "name": "graqle", "source": "./plugins/codex/graqle", "description": "Graph-powered codebase reasoning, impact analysis, and governed edits via the GraQle MCP server, plus governed-workflow skills.", - "version": "0.85.0", + "version": "0.85.1", "author": { "name": "Quantamix Solutions", "url": "https://graqle.com" diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 856137d3..f92f1d72 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "graqle", "source": "./plugins/claude-code/graqle", "description": "Graph-powered codebase reasoning, impact analysis, and governed edits via the GraQle MCP server, plus governed-workflow skills and an optional governance gate hook.", - "version": "0.85.0", + "version": "0.85.1", "author": { "name": "Quantamix Solutions", "url": "https://graqle.com" diff --git a/CHANGELOG.md b/CHANGELOG.md index aed76381..a64b283a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to GraQle are documented in this file. --- +## 0.85.1 — 2026-10-10 (Community edition message) + +### Fixed + +- **Community users are no longer told to install a package that does not exist.** Running a + hosted-service command such as `graq serve` on the free edition printed + `pip install graqle-studio`, but that distribution is not on PyPI, so the only suggested step + was a dead end. The message now says the command is part of the hosted GraQle service and is + not included in the free Community edition, and points to https://graqle.com/pricing + (Enterprise: sales@graqle.com). The free edition's capabilities are unchanged. + +> This patch contains only the edition-guard change (the guard hunk of CR-LIC-0851). The other +> CR-LIC-0851 changes (webhook, billing, register) ship in a later release after review. + +--- + ## 0.85.0 — 2026-09-27 (CI integrity) > **CI suppression status (CR-CI-001, issue #350).** The suppression ceiling on this repo diff --git a/graqle/__version__.py b/graqle/__version__.py index 7980df7c..6cfcf7f9 100644 --- a/graqle/__version__.py +++ b/graqle/__version__.py @@ -5,4 +5,4 @@ # constraints: none # ── /graqle:intelligence ── -__version__ = "0.85.0" +__version__ = "0.85.1" diff --git a/plugins/claude-code/graqle/.claude-plugin/plugin.json b/plugins/claude-code/graqle/.claude-plugin/plugin.json index afcbc948..48f67d11 100644 --- a/plugins/claude-code/graqle/.claude-plugin/plugin.json +++ b/plugins/claude-code/graqle/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "graqle", - "version": "0.85.0", + "version": "0.85.1", "description": "GraQle dev intelligence layer: graph-powered codebase reasoning, impact analysis, and governed edits via MCP, plus governed-workflow skills and an optional governance gate hook.", "author": { "name": "Quantamix Solutions", diff --git a/plugins/codex/graqle/.codex-plugin/plugin.json b/plugins/codex/graqle/.codex-plugin/plugin.json index acce4762..342effa2 100644 --- a/plugins/codex/graqle/.codex-plugin/plugin.json +++ b/plugins/codex/graqle/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "graqle", - "version": "0.85.0", + "version": "0.85.1", "description": "GraQle dev intelligence layer: graph-powered codebase reasoning, impact analysis, and governed edits via MCP, plus governed-workflow skills.", "interface": { "displayName": "GraQle", diff --git a/server.json b/server.json index e5ebda72..105e590f 100644 --- a/server.json +++ b/server.json @@ -8,12 +8,12 @@ "url": "https://github.com/quantamixsol/graqle", "source": "github" }, - "version": "0.85.0", + "version": "0.85.1", "packages": [ { "registryType": "pypi", "identifier": "graqle", - "version": "0.85.0", + "version": "0.85.1", "transport": { "type": "stdio" } From 1f289ea5102ae7e9f5b1574318e1ff22a0822221 Mon Sep 17 00:00:00 2001 From: quantamixsol Date: Sat, 10 Oct 2026 10:56:38 +0200 Subject: [PATCH 3/4] chore(release): pyproject version 0.85.1 (owner-approved via G4) Co-Authored-By: Claude Sonnet 5.5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4a26a3f0..ae6a280a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ name = "graqle" # (first-to-finish/best-of-N) + CostAwareRouter (auto cost/latency selection) — # dual-provider autonomy. 0.77.0 = ADR-239 Stage 2 # (CheckpointProtocol + run_tests). 0.76.0 = ADR-225 G1 multi-tenant memory. -version = "0.85.0" +version = "0.85.1" description = "Persistent organisational intelligence for AI agents. Turn codebases, documents, policies and decisions into a knowledge graph so Claude Code, Cursor and Copilot reason over architecture, dependencies and prior lessons — with confidence scores and evidence. 13 LLM backends + custom, fully offline capable." readme = "README_PYPI.md" license = {text = "Apache-2.0"} From b92ef5b54e227830428d043f645d285b0bde4f5c Mon Sep 17 00:00:00 2001 From: quantamixsol Date: Sat, 10 Oct 2026 11:04:39 +0200 Subject: [PATCH 4/4] fix: graq serve degrades cleanly on Community; graq studio names its own command - serve was unguarded: the Community wheel printed a raw ModuleNotFoundError traceback. Wrap with requires_package and resolve graqle.server.app up front so --reload/--workers reach the same message (find_spec None is treated as absent). - studio's degrade message hardcoded the label 'graq serve'. - Tests simulate the Community wheel by blocking the proprietary imports. Co-Authored-By: Claude Sonnet 5.5 --- graqle/cli/main.py | 13 ++++- .../test_community_serve_studio_degrade.py | 56 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/test_cli/test_community_serve_studio_degrade.py diff --git a/graqle/cli/main.py b/graqle/cli/main.py index c39d663a..2f21d99f 100644 --- a/graqle/cli/main.py +++ b/graqle/cli/main.py @@ -805,6 +805,7 @@ def inspect( @app.command() +@requires_package("graq serve") def serve( config: str = typer.Option("graqle.yaml", "--config", "-c"), host: str = typer.Option("0.0.0.0", "--host", "-h", help="Bind host"), @@ -838,6 +839,16 @@ def serve( ) raise typer.Exit(1) + # The API server is a proprietary backend, excluded from the Community wheel. + # Resolve it up front so a missing backend reaches the edition guard with a clean + # message, even under --reload/--workers where uvicorn imports it in a subprocess. + import importlib.util + + if importlib.util.find_spec("graqle.server.app") is None: + raise ModuleNotFoundError( + "No module named 'graqle.server.app'", name="graqle.server.app" + ) + # Forward config path and CWD to uvicorn factory subprocess via env vars import pathlib os.environ["GRAQLE_CONFIG_PATH"] = str(pathlib.Path(config).resolve()) @@ -938,7 +949,7 @@ def studio( from graqle.cli._edition_guard import _is_proprietary_absence, _degrade_message if _is_proprietary_absence(exc) is not None: - console.print(_degrade_message(exc.name or "graqle.studio", "graq serve")) + console.print(_degrade_message(exc.name or "graqle.studio", "graq studio")) raise typer.Exit(code=2) from None raise diff --git a/tests/test_cli/test_community_serve_studio_degrade.py b/tests/test_cli/test_community_serve_studio_degrade.py new file mode 100644 index 00000000..0e591b1c --- /dev/null +++ b/tests/test_cli/test_community_serve_studio_degrade.py @@ -0,0 +1,56 @@ +"""0.85.1: Community users get a clean message, not a traceback, from graq serve / studio. + +The Community wheel ships without the proprietary backends (graqle.server / graqle.studio). +These tests simulate that by making those imports fail, then drive the real CLI. +""" + +from __future__ import annotations + +import sys + +import pytest +from typer.testing import CliRunner + +from graqle.cli.main import app + +pytest.importorskip("uvicorn") +pytest.importorskip("fastapi") + +runner = CliRunner() + + +@pytest.fixture +def community_wheel(monkeypatch, tmp_path): + """Simulate the Community wheel: the proprietary packages cannot be imported.""" + for name in ("graqle.server", "graqle.server.app", "graqle.studio", "graqle.studio.app"): + monkeypatch.setitem(sys.modules, name, None) + monkeypatch.chdir(tmp_path) + + +def _assert_clean_degrade(result, command_label: str) -> None: + assert result.exit_code == 2, result.output + assert not isinstance(result.exception, ModuleNotFoundError) + assert "Traceback" not in result.output + assert f"'{command_label}'" in result.output + assert "pip install" not in result.output + assert "graqle.com/pricing" in result.output + + +def test_serve_degrades_cleanly_on_community(community_wheel): + result = runner.invoke(app, ["serve", "--port", "8099"]) + _assert_clean_degrade(result, "graq serve") + # Must fail before announcing a server start that can never happen. + assert "Server starting" not in result.output + + +def test_serve_with_reload_also_degrades_cleanly(community_wheel): + # uvicorn would import the app in a subprocess under --reload; the up-front + # resolution must catch the absent backend first. + result = runner.invoke(app, ["serve", "--reload", "--port", "8099"]) + _assert_clean_degrade(result, "graq serve") + + +def test_studio_names_its_own_command(community_wheel): + result = runner.invoke(app, ["studio", "--no-browser", "--port", "8098"]) + _assert_clean_degrade(result, "graq studio") + assert "'graq serve'" not in result.output