Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion graqle/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
# constraints: none
# ── /graqle:intelligence ──

__version__ = "0.85.0"
__version__ = "0.85.1"
24 changes: 14 additions & 10 deletions graqle/cli/_edition_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
Expand Down
13 changes: 12 additions & 1 deletion graqle/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion plugins/claude-code/graqle/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/graqle/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
56 changes: 56 additions & 0 deletions tests/test_cli/test_community_serve_studio_degrade.py
Original file line number Diff line number Diff line change
@@ -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
9 changes: 5 additions & 4 deletions tests/test_cli/test_edition_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----------------------------------------------
Expand Down
Loading