diff --git a/CLAUDE.md b/CLAUDE.md index 44f74cf6..dcb28b45 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -249,7 +249,6 @@ Transport is selectable via the global `--transport {auto,http,uds,mcp}` flag (o |---------|-------------| | `agent-brain install-agent --agent claude` | Install for Claude Code | | `agent-brain install-agent --agent opencode` | Install for OpenCode | -| `agent-brain install-agent --agent gemini` | Install for Gemini CLI | | `agent-brain install-agent --agent codex` | Install for Codex (+ AGENTS.md) | | `agent-brain install-agent --agent skill-runtime --dir ` | Install for any skill-based runtime | | `agent-brain install-agent --agent --dry-run` | Preview installation | diff --git a/TODO.md b/TODO.md index 62a0d15b..cf92187a 100644 --- a/TODO.md +++ b/TODO.md @@ -22,7 +22,7 @@ The same auto-registration is extended to the other runtimes (each has its own M - [x] [#224](https://github.com/SpillwaveSolutions/agent-brain/issues/224) — OpenCode (project-root `opencode.json` `mcp`) — shipped (#230) - [x] [#226](https://github.com/SpillwaveSolutions/agent-brain/issues/226) — Codex (`~/.codex/config.toml` `[mcp_servers]`) — shipped -- [ ] [#231](https://github.com/SpillwaveSolutions/agent-brain/issues/231) — **Remove** the `gemini` runtime (Google deprecated the Gemini CLI; supersedes the closed #225) +- [x] [#231](https://github.com/SpillwaveSolutions/agent-brain/issues/231) — **Removed** the `gemini` runtime (Google deprecated the Gemini CLI; supersedes the closed #225) — shipped > The native MCP server (formerly tracked here as #153/#167) shipped in the v10.1–v10.4 line > and is no longer pending. The full MCP v1–v4 roadmap is complete as of v10.4.0. diff --git a/agent-brain-cli/agent_brain_cli/commands/install_agent.py b/agent-brain-cli/agent_brain_cli/commands/install_agent.py index 32ee81c7..286554b6 100644 --- a/agent-brain-cli/agent_brain_cli/commands/install_agent.py +++ b/agent-brain-cli/agent_brain_cli/commands/install_agent.py @@ -12,7 +12,6 @@ from agent_brain_cli.runtime.claude_converter import ClaudeConverter from agent_brain_cli.runtime.codex_converter import CodexConverter -from agent_brain_cli.runtime.gemini_converter import GeminiConverter from agent_brain_cli.runtime.mcp_registration import ( McpRegistrationResult, register_claude_mcp, @@ -36,10 +35,6 @@ "project": ".opencode/plugins/agent-brain", "global": "~/.config/opencode/plugins/agent-brain", }, - "gemini": { - "project": ".gemini/plugins/agent-brain", - "global": "~/.config/gemini/plugins/agent-brain", - }, "codex": { "project": ".codex/skills/agent-brain", "global": "~/.codex/skills/agent-brain", @@ -50,17 +45,12 @@ DIR_REQUIRED_RUNTIMES = {"skill-runtime"} ConverterType = type[ - ClaudeConverter - | OpenCodeConverter - | GeminiConverter - | SkillRuntimeConverter - | CodexConverter + ClaudeConverter | OpenCodeConverter | SkillRuntimeConverter | CodexConverter ] CONVERTERS: dict[str, ConverterType] = { "claude": ClaudeConverter, "opencode": OpenCodeConverter, - "gemini": GeminiConverter, "skill-runtime": SkillRuntimeConverter, "codex": CodexConverter, } @@ -102,7 +92,7 @@ def _resolve_target_dir( return project_root / dir_template -RUNTIME_CHOICES = ["claude", "opencode", "gemini", "skill-runtime", "codex"] +RUNTIME_CHOICES = ["claude", "opencode", "skill-runtime", "codex"] # Runtimes for which we can auto-register the MCP server today, mapped to the # writer that knows that runtime's config schema. All writers share the @@ -274,7 +264,7 @@ def install_agent_command( Examples: agent-brain install-agent --agent claude --project agent-brain install-agent --agent opencode --global - agent-brain install-agent --agent gemini --dry-run + agent-brain install-agent --agent claude --dry-run agent-brain install-agent --agent skill-runtime --dir ./my-skills agent-brain install-agent --agent codex """ @@ -396,11 +386,7 @@ def install_agent_command( def _handle_dry_run( converter: ( - ClaudeConverter - | OpenCodeConverter - | GeminiConverter - | SkillRuntimeConverter - | CodexConverter + ClaudeConverter | OpenCodeConverter | SkillRuntimeConverter | CodexConverter ), bundle: Any, target: Path, diff --git a/agent-brain-cli/agent_brain_cli/runtime/__init__.py b/agent-brain-cli/agent_brain_cli/runtime/__init__.py index 5158d213..a238bcfc 100644 --- a/agent-brain-cli/agent_brain_cli/runtime/__init__.py +++ b/agent-brain-cli/agent_brain_cli/runtime/__init__.py @@ -12,7 +12,6 @@ ) from agent_brain_cli.runtime.tool_maps import ( CLAUDE_TOOLS, - GEMINI_TOOLS, OPENCODE_TOOLS, map_tool_name, map_tools, @@ -33,7 +32,6 @@ __all__ = [ "CLAUDE_TOOLS", - "GEMINI_TOOLS", "OPENCODE_TOOLS", "PluginAgent", "PluginBundle", diff --git a/agent-brain-cli/agent_brain_cli/runtime/gemini_converter.py b/agent-brain-cli/agent_brain_cli/runtime/gemini_converter.py deleted file mode 100644 index aca7a93d..00000000 --- a/agent-brain-cli/agent_brain_cli/runtime/gemini_converter.py +++ /dev/null @@ -1,119 +0,0 @@ -"""Gemini CLI runtime converter. - -Gemini uses different tool names and doesn't support some fields: -- Tool names: Read→read_file, Write→write_file, Edit→replace, Bash→run_shell_command -- No `color` field support -- Skills use `tools` list with Gemini-mapped names -""" - -import logging -from pathlib import Path - -import yaml - -from agent_brain_cli.runtime.tool_maps import map_tools -from agent_brain_cli.runtime.types import ( - PluginAgent, - PluginBundle, - PluginCommand, - PluginSkill, - RuntimeType, - Scope, -) - -logger = logging.getLogger(__name__) - -LEGACY_PATH = ".claude/agent-brain" -NEW_PATH = ".agent-brain" - - -def _replace_paths(text: str) -> str: - return text.replace(LEGACY_PATH, NEW_PATH) - - -def _rebuild_file(frontmatter: dict, body: str) -> str: # type: ignore[type-arg] - yaml_str = yaml.dump(frontmatter, default_flow_style=False, sort_keys=False) - return f"---\n{yaml_str}---\n{body}\n" - - -class GeminiConverter: - """Converter for Gemini CLI runtime.""" - - @property - def runtime_type(self) -> RuntimeType: - return RuntimeType.GEMINI - - def convert_command(self, command: PluginCommand) -> str: - fm: dict[str, object] = { - "name": command.name, - "description": command.description, - "parameters": [ - { - "name": p.name, - "description": p.description, - "required": p.required, - **({"default": p.default} if p.default else {}), - } - for p in command.parameters - ], - "skills": command.skills, - } - return _rebuild_file(fm, _replace_paths(command.body)) - - def convert_agent(self, agent: PluginAgent) -> str: - fm: dict[str, object] = { - "name": agent.name, - "description": agent.description, - "triggers": [ - {"pattern": t.pattern, "type": t.type} for t in agent.triggers - ], - "skills": agent.skills, - } - return _rebuild_file(fm, _replace_paths(agent.body)) - - def convert_skill(self, skill: PluginSkill) -> str: - """Convert skill with Gemini-mapped tool names, no color field.""" - mapped_tools = map_tools(skill.allowed_tools, "gemini") - # Filter out unsupported metadata fields - clean_metadata = {k: v for k, v in skill.metadata.items() if k != "color"} - fm: dict[str, object] = { - "name": skill.name, - "description": skill.description, - "license": skill.license, - "allowed-tools": mapped_tools, - "metadata": clean_metadata, - } - return _rebuild_file(fm, _replace_paths(skill.body)) - - def install( - self, - bundle: PluginBundle, - target_dir: Path, - scope: Scope, - ) -> list[Path]: - """Install Gemini CLI plugin files.""" - created: list[Path] = [] - - cmds_dir = target_dir / "commands" - cmds_dir.mkdir(parents=True, exist_ok=True) - for cmd in bundle.commands: - out = cmds_dir / f"{cmd.name}.md" - out.write_text(self.convert_command(cmd), encoding="utf-8") - created.append(out) - - agents_dir = target_dir / "agents" - agents_dir.mkdir(parents=True, exist_ok=True) - for agent in bundle.agents: - out = agents_dir / f"{agent.name}.md" - out.write_text(self.convert_agent(agent), encoding="utf-8") - created.append(out) - - skills_dir = target_dir / "skills" - for skill in bundle.skills: - skill_out = skills_dir / skill.name - skill_out.mkdir(parents=True, exist_ok=True) - skill_file = skill_out / "SKILL.md" - skill_file.write_text(self.convert_skill(skill), encoding="utf-8") - created.append(skill_file) - - return created diff --git a/agent-brain-cli/agent_brain_cli/runtime/tool_maps.py b/agent-brain-cli/agent_brain_cli/runtime/tool_maps.py index 9e11edb3..fe11c4a2 100644 --- a/agent-brain-cli/agent_brain_cli/runtime/tool_maps.py +++ b/agent-brain-cli/agent_brain_cli/runtime/tool_maps.py @@ -36,25 +36,10 @@ "TodoWrite": "todowrite", } -# Gemini CLI — different tool name convention -GEMINI_TOOLS: dict[str, str] = { - "Bash": "run_shell_command", - "Read": "read_file", - "Write": "write_file", - "Edit": "replace", - "Glob": "glob", - "Grep": "grep", - "Agent": "agent", - "WebFetch": "web_fetch", - "WebSearch": "web_search", - "NotebookEdit": "notebook_edit", -} - # Mapping from RuntimeType to tool map TOOL_MAPS: dict[str, dict[str, str]] = { "claude": CLAUDE_TOOLS, "opencode": OPENCODE_TOOLS, - "gemini": GEMINI_TOOLS, } @@ -67,7 +52,7 @@ def map_tool_name(tool: str, runtime: str) -> str: Args: tool: Canonical tool name (e.g., "Bash", "Write(.agent-brain/**)"). - runtime: Target runtime ("claude", "opencode", "gemini"). + runtime: Target runtime ("claude", "opencode"). Returns: Mapped tool name, or the lowercased base name if no mapping exists. diff --git a/agent-brain-cli/agent_brain_cli/runtime/types.py b/agent-brain-cli/agent_brain_cli/runtime/types.py index b52bf272..046781c4 100644 --- a/agent-brain-cli/agent_brain_cli/runtime/types.py +++ b/agent-brain-cli/agent_brain_cli/runtime/types.py @@ -9,7 +9,6 @@ class RuntimeType(str, Enum): CLAUDE = "claude" OPENCODE = "opencode" - GEMINI = "gemini" SKILL_RUNTIME = "skill-runtime" CODEX = "codex" diff --git a/agent-brain-cli/tests/test_install_agent.py b/agent-brain-cli/tests/test_install_agent.py index 8489a613..db87f856 100644 --- a/agent-brain-cli/tests/test_install_agent.py +++ b/agent-brain-cli/tests/test_install_agent.py @@ -98,24 +98,6 @@ def test_opencode_project_install( # OpenCode uses singular 'command/' directory (not 'commands/') assert (target / "command" / "agent-brain-search.md").exists() - def test_gemini_project_install( - self, runner: CliRunner, plugin_dir: Path, tmp_path: Path - ) -> None: - result = runner.invoke( - install_agent_command, - [ - "--agent", - "gemini", - "--plugin-dir", - str(plugin_dir), - "--path", - str(tmp_path), - ], - ) - assert result.exit_code == 0 - target = tmp_path / ".gemini" / "plugins" / "agent-brain" - assert (target / "commands" / "agent-brain-search.md").exists() - def test_dry_run(self, runner: CliRunner, plugin_dir: Path, tmp_path: Path) -> None: result = runner.invoke( install_agent_command, diff --git a/agent-brain-cli/tests/test_install_agent_mcp.py b/agent-brain-cli/tests/test_install_agent_mcp.py index cdc71676..3089ca78 100644 --- a/agent-brain-cli/tests/test_install_agent_mcp.py +++ b/agent-brain-cli/tests/test_install_agent_mcp.py @@ -105,11 +105,13 @@ def test_with_mcp_unsupported_runtime_warns_but_succeeds( install_agent_command, [ "--agent", - "gemini", + "skill-runtime", "--plugin-dir", str(plugin_dir), "--path", str(tmp_path), + "--dir", + str(tmp_path / "skills"), "--with-mcp", ], ) diff --git a/agent-brain-cli/tests/test_runtime_converters.py b/agent-brain-cli/tests/test_runtime_converters.py index a05e66e1..c24fd74b 100644 --- a/agent-brain-cli/tests/test_runtime_converters.py +++ b/agent-brain-cli/tests/test_runtime_converters.py @@ -1,4 +1,4 @@ -"""Tests for runtime converters (Claude, OpenCode, Gemini).""" +"""Tests for runtime converters (Claude, OpenCode).""" import json from pathlib import Path @@ -7,7 +7,6 @@ import yaml from agent_brain_cli.runtime.claude_converter import ClaudeConverter -from agent_brain_cli.runtime.gemini_converter import GeminiConverter from agent_brain_cli.runtime.opencode_converter import ( OpenCodeConverter, _color_to_hex, @@ -412,58 +411,6 @@ def test_tool_map_strips_path_scope(self) -> None: assert map_tool_name("mcp__server__tool", "opencode") == "mcp__server__tool" -class TestGeminiConverter: - """Tests for Gemini runtime converter.""" - - def test_runtime_type(self) -> None: - converter = GeminiConverter() - assert converter.runtime_type == RuntimeType.GEMINI - - def test_convert_skill_maps_tools(self, sample_skill: PluginSkill) -> None: - converter = GeminiConverter() - result = converter.convert_skill(sample_skill) - _, fm_text = result.split("---\n", 1) - fm_text = fm_text.split("---\n", 1)[0] - parsed = yaml.safe_load(fm_text) - tools = parsed["allowed-tools"] - assert "run_shell_command" in tools # Bash -> run_shell_command - assert "read_file" in tools # Read -> read_file - - def test_convert_skill_removes_color(self) -> None: - skill = PluginSkill( - name="test", - description="Test", - allowed_tools=["Bash"], - metadata={"version": "1.0", "color": "red"}, - body="Content", - ) - converter = GeminiConverter() - result = converter.convert_skill(skill) - _, fm_text = result.split("---\n", 1) - fm_text = fm_text.split("---\n", 1)[0] - parsed = yaml.safe_load(fm_text) - assert "color" not in parsed.get("metadata", {}) - - def test_convert_command_replaces_paths( - self, sample_command: PluginCommand - ) -> None: - converter = GeminiConverter() - result = converter.convert_command(sample_command) - assert ".agent-brain" in result - assert ".claude/agent-brain" not in result - - def test_install_creates_files( - self, tmp_path: Path, sample_bundle: PluginBundle - ) -> None: - converter = GeminiConverter() - target = tmp_path / "output" - files = converter.install(sample_bundle, target, Scope.PROJECT) - assert len(files) > 0 - assert (target / "commands" / "test-search.md").exists() - assert (target / "agents" / "search-helper.md").exists() - assert (target / "skills" / "using-agent-brain" / "SKILL.md").exists() - - class TestRoundTrip: """Round-trip tests: parse canonical → convert → verify structure.""" @@ -491,13 +438,3 @@ def test_opencode_round_trip(self, real_plugin_dir: Path | None) -> None: for skill in bundle.skills: result = converter.convert_skill(skill) assert "tools:" in result - - def test_gemini_round_trip(self, real_plugin_dir: Path | None) -> None: - if real_plugin_dir is None: - pytest.skip("Real plugin dir not found") - bundle = parse_plugin_dir(real_plugin_dir) - converter = GeminiConverter() - for skill in bundle.skills: - result = converter.convert_skill(skill) - # Bash should become run_shell_command - assert "run_shell_command" in result diff --git a/agent-brain-cli/tests/test_runtime_integration.py b/agent-brain-cli/tests/test_runtime_integration.py index 40c31af3..75efcd0d 100644 --- a/agent-brain-cli/tests/test_runtime_integration.py +++ b/agent-brain-cli/tests/test_runtime_integration.py @@ -7,7 +7,6 @@ from agent_brain_cli.runtime.claude_converter import ClaudeConverter from agent_brain_cli.runtime.codex_converter import CodexConverter -from agent_brain_cli.runtime.gemini_converter import GeminiConverter from agent_brain_cli.runtime.opencode_converter import OpenCodeConverter from agent_brain_cli.runtime.parser import parse_plugin_dir from agent_brain_cli.runtime.skill_runtime_converter import SkillRuntimeConverter @@ -55,23 +54,6 @@ def test_opencode_produces_tools_objects( assert "tools:" in content assert len(files) > 30 - def test_gemini_maps_tool_names( - self, real_plugin_dir: Path, tmp_path: Path - ) -> None: - bundle = parse_plugin_dir(real_plugin_dir) - converter = GeminiConverter() - target = tmp_path / "gemini" - files = converter.install(bundle, target, Scope.PROJECT) - - # Check skills use Gemini tool names - for skill in bundle.skills: - skill_file = target / "skills" / skill.name / "SKILL.md" - if skill_file.exists(): - content = skill_file.read_text() - if "Bash" in str(skill.allowed_tools): - assert "run_shell_command" in content - assert len(files) > 30 - def test_skill_runtime_flattens_to_skill_dirs( self, real_plugin_dir: Path, tmp_path: Path ) -> None: @@ -140,7 +122,6 @@ def test_all_converters_replace_legacy_paths( converters = [ ("claude", ClaudeConverter()), ("opencode", OpenCodeConverter()), - ("gemini", GeminiConverter()), ("skill-runtime", SkillRuntimeConverter()), ] diff --git a/agent-brain-cli/tests/test_runtime_parser.py b/agent-brain-cli/tests/test_runtime_parser.py index dbb762fd..84860ee0 100644 --- a/agent-brain-cli/tests/test_runtime_parser.py +++ b/agent-brain-cli/tests/test_runtime_parser.py @@ -15,7 +15,6 @@ ) from agent_brain_cli.runtime.tool_maps import ( CLAUDE_TOOLS, - GEMINI_TOOLS, OPENCODE_TOOLS, map_tool_name, map_tools, @@ -337,16 +336,9 @@ def test_opencode_lowercase(self) -> None: assert map_tool_name("Read", "opencode") == "read" assert map_tool_name("WebFetch", "opencode") == "web_fetch" - def test_gemini_mapping(self) -> None: - assert map_tool_name("Bash", "gemini") == "run_shell_command" - assert map_tool_name("Read", "gemini") == "read_file" - assert map_tool_name("Write", "gemini") == "write_file" - assert map_tool_name("Edit", "gemini") == "replace" - def test_unknown_tool_passthrough(self) -> None: # Unknown tools are lowercased (fallback behavior) assert map_tool_name("CustomTool", "claude") == "customtool" - assert map_tool_name("CustomTool", "gemini") == "customtool" # MCP tools pass through unchanged assert map_tool_name("mcp__server__tool", "opencode") == "mcp__server__tool" @@ -354,14 +346,13 @@ def test_unknown_runtime_uses_claude(self) -> None: assert map_tool_name("Bash", "unknown") == "Bash" def test_map_tools_list(self) -> None: - result = map_tools(["Bash", "Read"], "gemini") - assert result == ["run_shell_command", "read_file"] + result = map_tools(["Bash", "Read"], "opencode") + assert result == ["bash", "read"] def test_all_maps_have_same_keys(self) -> None: # OPENCODE_TOOLS has extra Claude-specific tools # (AskUserQuestion, SkillTool, TodoWrite) not in other runtimes assert set(CLAUDE_TOOLS.keys()).issubset(set(OPENCODE_TOOLS.keys())) - assert set(CLAUDE_TOOLS.keys()) == set(GEMINI_TOOLS.keys()) class TestRuntimeTypes: @@ -370,7 +361,6 @@ class TestRuntimeTypes: def test_runtime_types(self) -> None: assert RuntimeType.CLAUDE.value == "claude" assert RuntimeType.OPENCODE.value == "opencode" - assert RuntimeType.GEMINI.value == "gemini" def test_scope_types(self) -> None: assert Scope.PROJECT.value == "project" diff --git a/agent-brain-plugin/agents/setup-assistant.md b/agent-brain-plugin/agents/setup-assistant.md index 0df8cbff..88896b21 100644 --- a/agent-brain-plugin/agents/setup-assistant.md +++ b/agent-brain-plugin/agents/setup-assistant.md @@ -298,7 +298,6 @@ agent-brain install-agent --agent claude agent-brain install-agent --agent opencode # Install for Gemini -agent-brain install-agent --agent gemini # Install for Codex (skill directories + AGENTS.md) agent-brain install-agent --agent codex diff --git a/agent-brain-plugin/commands/agent-brain-install-agent.md b/agent-brain-plugin/commands/agent-brain-install-agent.md index d7f927f0..0ae15073 100644 --- a/agent-brain-plugin/commands/agent-brain-install-agent.md +++ b/agent-brain-plugin/commands/agent-brain-install-agent.md @@ -3,7 +3,7 @@ name: agent-brain-install-agent description: Install Agent Brain plugin for a specific runtime (Claude, OpenCode, Gemini) parameters: - name: agent - description: "Target runtime: claude, opencode, gemini, skill-runtime, or codex" + description: "Target runtime: claude, opencode, skill-runtime, or codex" required: true - name: scope description: "Install scope: project (default) or global" @@ -63,7 +63,7 @@ agent-brain install-agent --agent [--project|--global] [--plugin-dir < | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| -| --agent / -a | Yes | - | Target runtime: `claude`, `opencode`, `gemini`, `skill-runtime`, or `codex` | +| --agent / -a | Yes | - | Target runtime: `claude`, `opencode`, `skill-runtime`, or `codex` | | --project | No | Yes | Install to project directory (default) | | --global | No | No | Install to user-level directory | | --plugin-dir | No | Auto-detect | Custom canonical plugin source directory | @@ -99,7 +99,6 @@ agent-brain install-agent --agent opencode --project ### Install for Gemini CLI ```bash -agent-brain install-agent --agent gemini --project ``` ### Install for Codex @@ -231,7 +230,7 @@ agent-brain install-agent --agent claude --with-mcp --mcp-backend uds | Error | Cause | Resolution | |-------|-------|------------| | Could not find canonical plugin directory | Plugin source not found | Use `--plugin-dir` to specify location | -| Invalid agent choice | Unsupported runtime name | Use `claude`, `opencode`, `gemini`, `skill-runtime`, or `codex` | +| Invalid agent choice | Unsupported runtime name | Use `claude`, `opencode`, `skill-runtime`, or `codex` | | --dir is required for --agent skill-runtime | Missing target directory | Specify `--dir ./path/to/skills` | ## Notes diff --git a/agent-brain-plugin/skills/configuring-agent-brain/SKILL.md b/agent-brain-plugin/skills/configuring-agent-brain/SKILL.md index 2cec6193..501e0ef7 100644 --- a/agent-brain-plugin/skills/configuring-agent-brain/SKILL.md +++ b/agent-brain-plugin/skills/configuring-agent-brain/SKILL.md @@ -47,7 +47,6 @@ Agent Brain supports multiple AI coding runtimes from a single canonical plugin |---------|----------------| | Claude Code | `agent-brain install-agent --agent claude` | | OpenCode | `agent-brain install-agent --agent opencode` | -| Gemini CLI | `agent-brain install-agent --agent gemini` | | Codex (+ AGENTS.md) | `agent-brain install-agent --agent codex` | | Any skill runtime | `agent-brain install-agent --agent skill-runtime --dir ` | diff --git a/agent-brain-plugin/skills/configuring-agent-brain/references/configuration-guide.md b/agent-brain-plugin/skills/configuring-agent-brain/references/configuration-guide.md index 876134df..937c9011 100644 --- a/agent-brain-plugin/skills/configuring-agent-brain/references/configuration-guide.md +++ b/agent-brain-plugin/skills/configuring-agent-brain/references/configuration-guide.md @@ -683,7 +683,6 @@ Install the Agent Brain plugin into different AI coding assistant runtimes: ```bash agent-brain install-agent --agent claude # Claude Code agent-brain install-agent --agent opencode # OpenCode -agent-brain install-agent --agent gemini # Gemini agent-brain install-agent --agent codex # Codex agent-brain install-agent --agent skill-runtime --dir /path # Generic ``` diff --git a/agent-brain-plugin/skills/configuring-agent-brain/references/installation-guide.md b/agent-brain-plugin/skills/configuring-agent-brain/references/installation-guide.md index c7a701c0..e127e2ae 100644 --- a/agent-brain-plugin/skills/configuring-agent-brain/references/installation-guide.md +++ b/agent-brain-plugin/skills/configuring-agent-brain/references/installation-guide.md @@ -403,7 +403,6 @@ agent-brain install-agent --agent claude agent-brain install-agent --agent opencode # Install for Gemini -agent-brain install-agent --agent gemini # Install for Codex (generates skill directories + AGENTS.md) agent-brain install-agent --agent codex diff --git a/agent-brain-plugin/skills/configuring-agent-brain/references/mcp-setup-guide.md b/agent-brain-plugin/skills/configuring-agent-brain/references/mcp-setup-guide.md index c54aa145..22bedd8e 100644 --- a/agent-brain-plugin/skills/configuring-agent-brain/references/mcp-setup-guide.md +++ b/agent-brain-plugin/skills/configuring-agent-brain/references/mcp-setup-guide.md @@ -110,7 +110,7 @@ the table in Option A for each runtime's config location and schema). For other Desktop, Cursor, Windsurf), register manually using the Option B JSON. The flag prints a note and skips for runtimes it can't register rather than failing. -> The `gemini` runtime is being removed (Google deprecated the Gemini CLI — see +> The `gemini` runtime has been removed (Google deprecated the Gemini CLI — see > [#231](https://github.com/SpillwaveSolutions/agent-brain/issues/231)). ## Troubleshooting diff --git a/agent-brain-plugin/skills/using-agent-brain/references/api_reference.md b/agent-brain-plugin/skills/using-agent-brain/references/api_reference.md index 1591ce32..35fb1107 100644 --- a/agent-brain-plugin/skills/using-agent-brain/references/api_reference.md +++ b/agent-brain-plugin/skills/using-agent-brain/references/api_reference.md @@ -414,7 +414,6 @@ agent-brain config set embedding.provider openai # Set a config value ```bash agent-brain install-agent --agent claude # Install for Claude agent-brain install-agent --agent opencode # Install for OpenCode -agent-brain install-agent --agent gemini # Install for Gemini agent-brain install-agent --agent codex # Install for Codex agent-brain install-agent --agent skill-runtime --dir /path # Generic agent-brain install-agent --agent claude --dry-run # Preview diff --git a/agent-brain-plugin/skills/using-agent-brain/references/installation-guide.md b/agent-brain-plugin/skills/using-agent-brain/references/installation-guide.md index 9516fac1..95d5d165 100644 --- a/agent-brain-plugin/skills/using-agent-brain/references/installation-guide.md +++ b/agent-brain-plugin/skills/using-agent-brain/references/installation-guide.md @@ -297,7 +297,6 @@ agent-brain install-agent --agent claude agent-brain install-agent --agent opencode # Install for Gemini -agent-brain install-agent --agent gemini # Install for Codex (skill-directory format with AGENTS.md) agent-brain install-agent --agent codex diff --git a/agent-brain-plugin/skills/using-agent-brain/references/interactive-setup.md b/agent-brain-plugin/skills/using-agent-brain/references/interactive-setup.md index 5f35e7c6..f08ce9fa 100644 --- a/agent-brain-plugin/skills/using-agent-brain/references/interactive-setup.md +++ b/agent-brain-plugin/skills/using-agent-brain/references/interactive-setup.md @@ -143,7 +143,6 @@ Install the plugin for your AI coding assistant: ```bash agent-brain install-agent --agent claude # Claude Code agent-brain install-agent --agent opencode # OpenCode -agent-brain install-agent --agent gemini # Gemini agent-brain install-agent --agent codex # Codex ``` diff --git a/agent-brain-server/agent_brain_server/providers/embedding/openai.py b/agent-brain-server/agent_brain_server/providers/embedding/openai.py index 37d7d6ae..7cda2628 100644 --- a/agent-brain-server/agent_brain_server/providers/embedding/openai.py +++ b/agent-brain-server/agent_brain_server/providers/embedding/openai.py @@ -49,7 +49,10 @@ def __init__(self, config: "EmbeddingConfig") -> None: batch_size = config.params.get("batch_size", 100) super().__init__(model=config.model, batch_size=batch_size) - self._client = AsyncOpenAI(api_key=api_key) + self._client = AsyncOpenAI( + api_key=api_key, + base_url=config.get_base_url() or None, + ) self._dimensions_override = config.params.get("dimensions") @property diff --git a/agent-brain-server/agent_brain_server/providers/summarization/openai.py b/agent-brain-server/agent_brain_server/providers/summarization/openai.py index 14f1fde1..12565060 100644 --- a/agent-brain-server/agent_brain_server/providers/summarization/openai.py +++ b/agent-brain-server/agent_brain_server/providers/summarization/openai.py @@ -50,7 +50,10 @@ def __init__(self, config: "SummarizationConfig") -> None: prompt_template=prompt_template, ) - self._client = AsyncOpenAI(api_key=api_key) + self._client = AsyncOpenAI( + api_key=api_key, + base_url=config.get_base_url() or None, + ) @property def provider_name(self) -> str: diff --git a/agent-brain-server/tests/unit/providers/test_openai_embedding.py b/agent-brain-server/tests/unit/providers/test_openai_embedding.py index 4dde89e2..d20acd3a 100644 --- a/agent-brain-server/tests/unit/providers/test_openai_embedding.py +++ b/agent-brain-server/tests/unit/providers/test_openai_embedding.py @@ -157,3 +157,29 @@ def test_dimension_values(self) -> None: assert OPENAI_MODEL_DIMENSIONS["text-embedding-3-large"] == 3072 assert OPENAI_MODEL_DIMENSIONS["text-embedding-3-small"] == 1536 assert OPENAI_MODEL_DIMENSIONS["text-embedding-ada-002"] == 1536 + + +class TestOpenAIEmbeddingBaseUrl: + """Tests for OpenAI-compatible endpoint support (issue #222).""" + + @patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}) + def test_configured_base_url_is_passed_to_client(self) -> None: + """A configured base_url must reach the AsyncOpenAI client.""" + config = EmbeddingConfig( + provider="openai", + model="BAAI/bge-m3", + base_url="https://gateway.internal/v1", + ) + provider = OpenAIEmbeddingProvider(config) + + assert str(provider._client.base_url).rstrip("/") == ( + "https://gateway.internal/v1" + ) + + @patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}) + def test_no_base_url_keeps_openai_default(self) -> None: + """Without base_url the client keeps the standard OpenAI endpoint.""" + config = EmbeddingConfig(provider="openai", model="text-embedding-3-large") + provider = OpenAIEmbeddingProvider(config) + + assert "api.openai.com" in str(provider._client.base_url) diff --git a/agent-brain-server/tests/unit/providers/test_openai_summarization.py b/agent-brain-server/tests/unit/providers/test_openai_summarization.py new file mode 100644 index 00000000..e376bf1a --- /dev/null +++ b/agent-brain-server/tests/unit/providers/test_openai_summarization.py @@ -0,0 +1,60 @@ +"""Unit tests for OpenAI summarization provider.""" + +from unittest.mock import patch + +import pytest + +from agent_brain_server.config.provider_config import SummarizationConfig +from agent_brain_server.providers.exceptions import AuthenticationError +from agent_brain_server.providers.summarization.openai import ( + OpenAISummarizationProvider, +) + + +class TestOpenAISummarizationProvider: + """Tests for OpenAISummarizationProvider.""" + + @patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}) + def test_initialization(self) -> None: + """Test provider initialization.""" + config = SummarizationConfig(provider="openai", model="gpt-5-mini") + provider = OpenAISummarizationProvider(config) + + assert provider.provider_name == "OpenAI" + assert provider.model_name == "gpt-5-mini" + + def test_initialization_missing_key(self) -> None: + """Test error when API key is missing.""" + with patch.dict("os.environ", {}, clear=True): + config = SummarizationConfig( + provider="openai", + api_key_env="MISSING_KEY", + ) + with pytest.raises(AuthenticationError): + OpenAISummarizationProvider(config) + + +class TestOpenAISummarizationBaseUrl: + """Tests for OpenAI-compatible endpoint support (issue #222).""" + + @patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}) + def test_configured_base_url_is_passed_to_client(self) -> None: + """A configured base_url must reach the AsyncOpenAI client.""" + config = SummarizationConfig( + provider="openai", + model="gpt-5-mini", + base_url="https://gateway.internal/v1", + ) + provider = OpenAISummarizationProvider(config) + + assert str(provider._client.base_url).rstrip("/") == ( + "https://gateway.internal/v1" + ) + + @patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}) + def test_no_base_url_keeps_openai_default(self) -> None: + """Without base_url the client keeps the standard OpenAI endpoint.""" + config = SummarizationConfig(provider="openai", model="gpt-5-mini") + provider = OpenAISummarizationProvider(config) + + assert "api.openai.com" in str(provider._client.base_url) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 27ec9e09..0c25e6c0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [10.5.0] - 2026-08-30 + +### Fixed + +- **The OpenAI embedding and summarization providers now honor a configured `base_url`** (`agent-brain-server/agent_brain_server/providers/embedding/openai.py`, `providers/summarization/openai.py`; closes [#222](https://github.com/SpillwaveSolutions/agent-brain/issues/222)). Both providers constructed `AsyncOpenAI(api_key=api_key)` without passing the `base_url` from `config.yaml`, so every embedding and summarization request went to `api.openai.com` regardless of configuration — making Agent Brain unusable with OpenAI-compatible endpoints (LiteLLM, vLLM, LocalAI, Azure OpenAI, or an internal gateway) and failing with `APIConnectionError` in environments without direct OpenAI egress. Both now pass `base_url=config.get_base_url() or None`, matching what the Ollama and Grok providers already did; with no `base_url` configured the client keeps the standard OpenAI endpoint. Reported by @stevemju. + +- **Keycloak-in-CI OAuth E2E: the bootstrapped test user is now created fully set up** (`scripts/keycloak_bootstrap.sh`; addresses [#218](https://github.com/SpillwaveSolutions/agent-brain/issues/218)). `testuser` was created with only a username and password. Keycloak >= 24 enables the declarative User Profile with the "Verify Profile" required action on by default, which leaves such an account carrying a `VERIFY_PROFILE` required action and makes every Direct Access Grant fail with `HTTP 400 {"error":"invalid_grant","error_description":"Account is not fully set up"}` — matching the observed failure exactly (all 8 external-IdP tests dying at setup against the token endpoint, before any MCP logic runs). The user is now created with `email`, `emailVerified`, `firstName`, `lastName`, and an explicit empty `requiredActions`. *Verification note: this could not be exercised locally (no container runtime available); the `MCP Keycloak Integration` CI job is the confirming run.* + +### Removed + +- **The `gemini` runtime is no longer a target for `agent-brain install-agent`** (closes [#231](https://github.com/SpillwaveSolutions/agent-brain/issues/231), supersedes the closed #225). Google deprecated the Gemini CLI, so rather than extend `--with-mcp` auto-registration to it, the runtime is removed: `runtime/gemini_converter.py` is deleted along with the `GEMINI_TOOLS` tool map, the `RuntimeType.GEMINI` enum member, and the `gemini` entries in `RUNTIME_CHOICES` / `INSTALL_DIRS` / `CONVERTERS`. `agent-brain install-agent --agent gemini` is now rejected as an invalid choice, which lists the four supported runtimes (`claude`, `opencode`, `skill-runtime`, `codex`). + - **Gemini remains fully supported as an LLM provider.** This removal affects only the deprecated Gemini CLI as an *install target*; `summarization.provider: gemini` (and `GEMINI_API_KEY`) are untouched. + +--- + ## [10.4.0] - 2026-06-22 Closes the **v10.4 milestone — MCP v4: OAuth 2.1 + GraphRAG Stability** (7 phases, 64–70; 16/16 requirements; milestone audit passed). Agent Brain can now run remotely behind OAuth 2.1 on the Streamable HTTP transport — in both a co-located AS/RS single-binary shape and a split AS/RS shape backed by an external IdP (validated against Keycloak-in-CI). Bugs were fixed first: the kuzu `SIGSEGV` (#178) and graph under-reporting / stale-snapshot issues (#184). Auth is **off by default** (`AGENT_BRAIN_AUTH=none`); nothing changes for existing local/loopback users unless they opt in. diff --git a/docs/MCP_USER_GUIDE.md b/docs/MCP_USER_GUIDE.md index 901e5962..15721547 100644 --- a/docs/MCP_USER_GUIDE.md +++ b/docs/MCP_USER_GUIDE.md @@ -48,7 +48,7 @@ Complete reference for `agent-brain-mcp` — the Model Context Protocol server t > **Register it for Claude Code in one command:** `agent-brain install-agent --agent claude --with-mcp` > writes the `.mcp.json` entry for you (see [Register automatically](#register-automatically-claude-code)). -If you want slash commands inside Claude Code, OpenCode, Gemini CLI, or Codex, use the [plugin](./PLUGIN_GUIDE.md) instead. If your LLM client speaks MCP natively (Claude Desktop, Cursor, an agent SDK), use this server. +If you want slash commands inside Claude Code, OpenCode, or Codex, use the [plugin](./PLUGIN_GUIDE.md) instead. If your LLM client speaks MCP natively (Claude Desktop, Cursor, an agent SDK), use this server. --- @@ -58,14 +58,14 @@ You can run both at the same time against the same backend — they don't confli | Question | Plugin | MCP server | |---|---|---| -| Where does it run? | Inside Claude Code / OpenCode / Gemini CLI / Codex | Inside Claude Desktop, Cursor, Windsurf, an agent SDK process | +| Where does it run? | Inside Claude Code / OpenCode / Codex | Inside Claude Desktop, Cursor, Windsurf, an agent SDK process | | How does the user call it? | `/agent-brain-search "…"` slash commands (30 of them) | The host model picks tools, reads resources, or expands prompts as part of normal tool use | | What's installed? | Markdown files + a few shell commands; shells out to the `agent-brain` CLI | A Python process started by the host as a subprocess | | Best for | Interactive sessions where humans drive search via slash commands | Agentic / autonomous workflows where the model itself orchestrates retrieval | | Backend required? | Yes (`agent-brain start`) | Yes (`agent-brain start`) | -| Multi-runtime? | Yes — Claude Code, OpenCode, Gemini CLI, Codex, generic skill runtime | Yes — any MCP-aware host | +| Multi-runtime? | Yes — Claude Code, OpenCode, Codex, generic skill runtime | Yes — any MCP-aware host | -**Rule of thumb:** if your client has a `mcpServers` config block, use the MCP server. If it has a `plugins` config block and you're a Claude Code / OpenCode / Gemini user, use the plugin. +**Rule of thumb:** if your client has a `mcpServers` config block, use the MCP server. If it has a `plugins` config block and you're a Claude Code / OpenCode / Codex user, use the plugin. --- @@ -140,7 +140,7 @@ reports `unchanged`). Flags: `--with-mcp`, `--mcp-backend {auto,uds,http}`, Codex has no project-level MCP config, so both scopes write the user-level `config.toml`. For other hosts the flag prints a note and skips — register manually with the JSON below. (The `gemini` -runtime is being removed — Google deprecated the Gemini CLI; see +runtime has been removed — Google deprecated the Gemini CLI; see [#231](https://github.com/SpillwaveSolutions/agent-brain/issues/231).) ### Universal stdio config @@ -828,7 +828,7 @@ The full MCP roadmap is complete as of **v10.4**: What's next (not yet shipped): -- Multi-runtime `--with-mcp` auto-registration: OpenCode ✅ ([#224](https://github.com/SpillwaveSolutions/agent-brain/issues/224)) and Codex ✅ ([#226](https://github.com/SpillwaveSolutions/agent-brain/issues/226)) shipped; `gemini` runtime being removed ([#231](https://github.com/SpillwaveSolutions/agent-brain/issues/231)). +- Multi-runtime `--with-mcp` auto-registration: OpenCode ✅ ([#224](https://github.com/SpillwaveSolutions/agent-brain/issues/224)) and Codex ✅ ([#226](https://github.com/SpillwaveSolutions/agent-brain/issues/226)) shipped; `gemini` runtime removed ([#231](https://github.com/SpillwaveSolutions/agent-brain/issues/231)). - Enterprise hardening + cloud deployment — [#219](https://github.com/SpillwaveSolutions/agent-brain/issues/219) and follow-ups #200–#205. See [`docs/roadmaps/mcp/`](./roadmaps/mcp/) for the original per-version scope and @@ -838,7 +838,7 @@ See [`docs/roadmaps/mcp/`](./roadmaps/mcp/) for the original per-version scope a ## Related docs -- [Plugin Guide](./PLUGIN_GUIDE.md) — the slash-command companion for Claude Code / OpenCode / Gemini CLI / Codex. +- [Plugin Guide](./PLUGIN_GUIDE.md) — the slash-command companion for Claude Code / OpenCode / Codex. - [User Guide](./USER_GUIDE.md) — backend setup, indexing, retrieval modes, multi-instance architecture. - [API Reference](./API_REFERENCE.md) — FastAPI backend schemas (every MCP tool wraps one of these). - [Configuration](./CONFIGURATION.md) — provider configuration (embedding, summarization, reranker). diff --git a/docs/PLUGIN_GUIDE.md b/docs/PLUGIN_GUIDE.md index deeb5de5..95bd6f3c 100644 --- a/docs/PLUGIN_GUIDE.md +++ b/docs/PLUGIN_GUIDE.md @@ -316,7 +316,6 @@ Install Agent Brain plugin for a specific AI coding runtime. Converts the canoni ``` /agent-brain-install-agent --agent claude /agent-brain-install-agent --agent opencode --project -/agent-brain-install-agent --agent gemini --global /agent-brain-install-agent --agent claude --dry-run ``` diff --git a/docs/plans/research-graph-make-it-real.md b/docs/plans/research-graph-make-it-real.md new file mode 100644 index 00000000..d8adb416 --- /dev/null +++ b/docs/plans/research-graph-make-it-real.md @@ -0,0 +1,219 @@ +# Plan: Make research-graph real (Layer 1 projector → live Agent Brain wiring) + +**Date:** 2026-08-30 +**Repos involved:** [SpillwaveSolutions/research-graph](https://github.com/SpillwaveSolutions/research-graph) (primary), +[SpillwaveSolutions/agent-brain](https://github.com/SpillwaveSolutions/agent-brain) (one contained feature), +[SpillwaveSolutions/research-knowledge-capture](https://github.com/SpillwaveSolutions/research-knowledge-capture) (contract source, no changes). +**Informed by:** recent work across the OKF family — okf-plugin 0.8.x, okf-agent-graph (AGER) 0.8.x, RKC 0.2.x, okf-forge, okf-agent-graph-ui. + +--- + +## 1. Where research-graph stands today (v0.1.0) + +research-graph is deliberately a stub. Its own PRD says: "Stub projector writes +`projection/manifest.json`. Live Kuzu/Chroma wiring is the next ticket." Concretely: + +- `scripts/rg_project.py` scans RKC OKF frontmatter under `/research/**`, filters + `accepted|reviewed`, and writes `projection/manifest.json`. It never touches Chroma, BM25, + or Kuzu. +- `scripts/rg_ask.py` implements retrieval-ladder steps 1–2 only: ripgrep over the research + tree (recently landed as PR #2, mirroring okf-plugin's brand-new rg-backed backlinks + pattern, including the `OKF_RG_PATH`/`fake_rg.py` conventions) and shelling to RKC's + `rkc_pack.py`. Steps 3–4 print the literal string `"unprojected — run /research-project"`. +- `.work/todo.jsonl` names the two open items: "Wire Agent Brain Chroma + BM25 + Kuzu" and + "/research-ask live retrieval". +- Five-host packaging (Agent Plugins 1.0, Claude Code, Grok Build, Codex, Cursor) exists and + has the family's lockstep-version test — that part is already at family standard. +- Known defects: CI runs only `test_plugin.py` (not `test_ask.py`/`test_project.py`), and + `test_project.py` hard-codes `/workspace/repos/research-knowledge-capture/sample-knowledge` + so it silently skips everywhere else. `hooks/hooks.json` is an empty PostToolUse list. + +## 2. The contract research-graph must satisfy (from RKC, Layer 0) + +RKC v0.2.6 is real and battle-tested (its 0.2.1–0.2.6 releases are all hardening from a +2,997-file corpus run). Its PRD fixes the Layer 1 job precisely: + +- **L1 owns:** the Chroma + BM25 + Kuzu projector into Agent Brain, and `/research-ask`. + **L1 owns no nouns.** "Projector stays here until a second consumer needs the Protocol in core." +- **Project only `accepted|reviewed`** nodes (of RKC's 8 nouns: ResearchArea, Subject, + ResearchTask, SourceDocument, ResearchQuestion, Claim, Evidence, Finding; 12 registered + rels: `has_subject`, `related_to`, `has_task`, `ingested_from`, `asks`, `answers`, + `produced`, `asserts`, `evidenced_by`, `contradicts`, `supersedes`, `same_as`). +- **"Do not fork Agent Brain. `GRAPH_USE_LLM_EXTRACTION=false`."** The index must never + invent nodes or edges; extraction happened once, deterministically, in L0. +- **Citations resolve in OKF, never in the index.** The spine is + `Finding → asserts → Claim → evidenced_by → Evidence → source-asset + locator`. + "No Chroma/Kuzu blob citations." +- **Destroying the index is always safe.** Rebuild from `knowledge/research/**`. +- The projector **may expose `informs` as a query-only inverse** of content-media's + `Article → draws_from → Finding` edge, but never writes edges whose target types it + doesn't own. +- Retrieval ladder order is fixed: `rg` → `/research-pack` → BM25/Chroma → Kuzu last. + +## 3. What Agent Brain already provides (verified in this repo) + +The target side is largely done — this is why the wiring is tractable now: + +- **Vectors + lexical:** hybrid BM25 + Chroma retrieval via `POST /query`, with + `file_paths`, `source_types`, `entity_types`, `relationship_types` filters and + `mode`/`alpha` hybrid controls. Results carry `source` (file path) + `chunk_id`, so hits + can be mapped back to OKF locators. +- **Ingestion:** `POST /index/add` (folder-based, queued), `DELETE /index` (clear), + folders add/remove, job queue with status. +- **Graph:** Kuzu-backed property graph (`KuzuPropertyGraphStore` via LlamaIndex, with + `simple` JSON fallback), `GET /graph/entity/{type}/{id}` returning the entity plus 1-hop + incoming/outgoing neighbors, gated by `ENABLE_GRAPH_INDEX`. +- **Extraction can be fully disabled:** `GRAPH_DOC_EXTRACTOR="none"` plus + `GRAPH_USE_LLM_EXTRACTION=false` means no langextract, no LLM extractor — the exact + posture RKC demands. +- **Instance isolation:** project-mode instances with `AGENT_BRAIN_STATE_DIR`, UDS + transport, and the `agent-brain` CLI (`init`/`start`/`stop`/`status`) make a dedicated, + disposable per-knowledge-root index cheap — which is what makes "destroying the index is + always safe" honest. + +## 4. The real gaps + +### Gap A — nobody writes typed OKF edges into the graph (the one Agent Brain change) + +Agent Brain's graph vocabulary is the 17 SCHEMA-01 entity types (Package…Enum, +DesignDoc…APIDoc, Service…ConfigFile) and 8 relationship predicates (`calls`, `extends`, +`implements`, `references`, `depends_on`, `imports`, `contains`, `defined_in`). None of +RKC's 8 nouns or 12 rels exist there, and with extraction disabled markdown contributes +nothing to the graph at all. There is also no API to ingest *pre-typed* entities/relations +without extraction. + +The good news (verified): the Kuzu/simple backends store generic labeled property nodes — +the 17-type limit lives in the Pydantic `Literal` vocabulary and endpoint validation, not +in physical Kuzu tables. Extending it is a models/validation change, not a storage +migration. + +**Recommendation:** add a deterministic **projection ingestion path** to agent-brain-server — +`POST /graph/project` accepting explicit typed entities and relations +(`{entities: [{type, id, properties}], relations: [{src, predicate, dst}], source_tag}`), +upsert semantics, delete-by-`source_tag` for rebuilds — with the type/predicate vocabulary +made extensible (a registered-vocabulary setting or namespaced types, e.g. `okf:Claim`), +so SCHEMA-01 code/doc/infra types and OKF research nouns coexist without forking. This +keeps the *projector logic* in the research-graph plugin (per the RKC PRD) while Agent +Brain merely accepts explicit facts. The alternative — a built-in "frontmatter" doc +extractor in Agent Brain — spreads OKF knowledge into core and is not recommended while +research-graph is the only consumer. + +`GET /graph/entity/{type}/{id}` validation must accept the extended vocabulary so ladder +step 4 can traverse `Finding → asserts → Claim` paths. + +### Gap B — the projector doesn't project (research-graph work) + +`rg_project.py` must actually populate the index. Design that fits everything above: + +1. **Materialize a projection corpus** under `/projection/corpus/` — a filtered copy + of only `accepted|reviewed` node bodies (frontmatter preserved), one file per node, + named by node id. This is what gets handed to `/index/add`. It solves three problems at + once: the status filter (Agent Brain indexes folders wholesale; draft nodes must never + reach the index), locator mapping (each projected file records its OKF `path` so query + hits resolve back to real OKF locators), and rebuildability (delete `projection/` + + `DELETE /index` = clean slate). +2. **Manifest v2**: keep `projection/manifest.json` as the rebuild record, adding per-node + `source_hash` (sha256 of the node file) so re-projection is incremental and idempotent — + the same pattern as RKC's `catalogs/ingest-index.json` O(1) idempotency index (their + issue #1 fix; learn from it now rather than at 3k files). +3. **Graph projection**: emit RKC's typed `links` as explicit relations to + `POST /graph/project` (Gap A), tagged `source_tag: research-graph`, plus the + `informs` query-only inverse. No extraction anywhere. +4. **Instance management**: the projector ensures a per-knowledge-root Agent Brain + instance (`agent-brain init`/`start` or direct HTTP against `AGENT_BRAIN_URL`), started + with `GRAPH_DOC_EXTRACTOR=none`, `GRAPH_USE_LLM_EXTRACTION=false`, + `ENABLE_GRAPH_INDEX=true`, `AGENT_BRAIN_STATE_DIR=/projection/.agent-brain`. + Server absent/unreachable = report and exit nonzero for `/research-project`; for + `/research-ask` it is just a missing ladder rung (see Gap C). + +### Gap C — the ask ladder stops at step 2 (research-graph work) + +`rg_ask.py` steps 3–4 become live: + +- **Step 3 (BM25/Chroma):** `POST /query` (hybrid mode) scoped to the projection corpus + via the existing `file_paths` filter; map each hit's `source`/`chunk_id` back through the + manifest to `{node_id, okf_path}`. Output cites OKF locators; chunk text may be shown but + the citation is always the OKF path — never a blob id. +- **Step 4 (Kuzu):** resolve step-3 hit nodes via `GET /graph/entity/okf:/` and + walk the spine (`asserts`, `evidenced_by`, `answers`) for typed paths; used for + "why/how supported" questions and for pulling the citation spine when the packer can't. +- **Missing-server behavior mirrors missing-rg:** "not an error" — the ladder reports the + rung as unavailable (`index: unreachable — start with /research-project`) and lower rungs + still answer. This matches the family's fail-soft read path / fail-closed write path + split. + +### Gap D — quality and conventions parity with the recent OKF work + +The OKF family's last three weeks set a clear bar (okf-plugin 0.7.x–0.8.1, AGER +0.6.x–0.8.1, RKC 0.2.x). research-graph should match it: + +- **CI must run all tests.** Today ci.yml runs `py_compile` + `test_plugin.py` only. Add + `test_ask.py`, `test_project.py`, and the new projector/ladder tests. Fix + `test_project.py`'s hard-coded `/workspace/repos/...` path — discover the RKC sibling the + same way `rg_ask.py` does, or check out RKC in CI the way AGER's quality.yml checks out + okf-plugin (and pin a *current* tag; AGER's stale `v0.3.2` pin is a documented trap). +- **Test the wire without the server:** fake Agent Brain HTTP fixture (same spirit as + `tests/fixtures/fake_rg.py`), asserting: draft nodes never reach `/index/add`; rebuild + after destroy converges to the same manifest; citations resolve to OKF paths. +- **Worklog/WikiTicket SDD adoption:** okf-plugin, AGER, and RKC all run the vendored + worklog toolchain (ULID-stamped commits enforced by `hooks/commit-msg`, `.work/` ledgers, + version-lockstep release ritual). research-graph has a bare `.work/todo.jsonl` and no + hooks. Adopt the same `bin/` + git-hooks setup when the repo starts taking real traffic. +- **E2E acceptance run** against RKC's `sample-knowledge` (fiction corpus, pack root + `subject.loop-policy.01J8X000000000000000000001`): project → ask "false alert rate" → + answer cites `evidence.loop-policy…0007`'s locator into the archived source asset. + +### Gap E — ecosystem seams (no code now, but decide deliberately) + +- **Nobody else in the family knows research-graph exists.** Zero references in okf-plugin, + AGER, okf-forge, or okf-agent-graph-ui (verified by grep over trees and git history). + When v0.2 ships, add research-graph/RKC to okf-plugin's family roster docs + (ONBOARDING.md) — and note RKC's schemas are *not* in `okf_schema.py`'s hardcoded + sibling-discovery list, which matters for anyone validating a mixed second brain with + research nouns in it. +- **AGER's `RetrievalBinding backend: hybrid` / `KnowledgeBind query_mode: graph_expand` + are declared config with no engine anywhere.** research-graph's ask ladder is the natural + engine behind those bindings. Out of scope for v0.2; record it as the follow-on so the + two vocabularies don't drift apart. +- **okf-forge visualizes plain OKF markdown directories** (auto-detects `.okf/` / + `sample-okf/`), so the RKC tree is already viewable there for free — the projection + gains nothing from a Forge integration and none should be built. +- **Naming collision, docs-only:** "research graph" is also the *name of the sample + multi-agent loop* in okf-agent-graph (`sample-ager/index.md`, "Sample AGER research + graph") and okf-agent-graph-ui ("Parallel research graph"). Worth one disambiguating + line in research-graph's README. + +## 5. Sequenced work items + +**Phase A — vectors + lexical live (research-graph only; no Agent Brain changes):** +1. Projection corpus writer + manifest v2 (incremental via `source_hash`). +2. Instance management (`init`/`start`/health via CLI or HTTP; env-configurable URL). +3. `/index/add` wiring + `rg_ask` step 3 (`/query` + locator mapping). +4. Test fixtures (fake server), fix `test_project.py` path, wire all tests into CI. +5. Release v0.2.0 (lockstep bump, CHANGELOG, `.work/todo.jsonl` item 2 partially done). + +**Phase B — typed graph (agent-brain feature + research-graph consumption):** +6. agent-brain: `POST /graph/project` (explicit entities/relations, upsert, + delete-by-source_tag) + extensible/namespaced type vocabulary + `/graph/entity` + validation update. Spec'd via the normal speckit flow in this repo. +7. research-graph: project the 12 rels + `informs` inverse; `rg_ask` step 4 typed paths. +8. E2E acceptance run against `sample-knowledge`; release v0.3.0. + +**Phase C — family integration:** +9. Worklog/WikiTicket adoption, ULID commit hooks, family roster docs, README + disambiguation note, AGER retrieval-binding alignment ticket. + +## 6. Acceptance criteria ("real" means) + +- `/research-project` on RKC `sample-knowledge` populates a live per-root Agent Brain + instance (Chroma + BM25 + Kuzu), projecting exactly the `accepted|reviewed` nodes, with + zero LLM/langextract extraction. +- `/research-ask "false alert rate"` walks all four rungs and answers with citations that + resolve `Finding → Claim → Evidence → source-asset locator` in the OKF tree — never a + vector or graph blob. +- `DELETE` the index + delete `projection/` + re-run `/research-project` converges to an + identical manifest (destroy is always safe, rebuild is deterministic). +- Draft/rejected/superseded nodes are provably absent from the index (test-enforced). +- Missing rg, missing RKC, or missing server each degrade one rung without failing the + ladder. +- CI runs the full test suite on every push, including the projector and ladder tests. diff --git a/scripts/keycloak_bootstrap.sh b/scripts/keycloak_bootstrap.sh index aa2a5361..5f0a6a0f 100755 --- a/scripts/keycloak_bootstrap.sh +++ b/scripts/keycloak_bootstrap.sh @@ -129,12 +129,26 @@ log "Audience mapper added (aud will be '${RESOURCE}' in issued JWTs)." # --------------------------------------------------------------------------- # Step 5: Create a test user (testuser / testpass) +# +# email/firstName/lastName and the empty requiredActions list are load-bearing, +# not decoration. Keycloak >= 24 enables the declarative User Profile with the +# "Verify Profile" required action on by default: a user created with only a +# username and password is left carrying a VERIFY_PROFILE required action, and +# every Direct Access Grant for it fails with +# HTTP 400 {"error":"invalid_grant","error_description":"Account is not fully set up"} +# which is the whole-suite-at-setup failure tracked in #218. Populating the +# profile fields and clearing requiredActions keeps the account fully set up. # --------------------------------------------------------------------------- log "Creating test user '${TEST_USER}'..." _admin POST "/admin/realms/${REALM}/users" \ "{ \"username\": \"${TEST_USER}\", \"enabled\": true, + \"email\": \"${TEST_USER}@example.com\", + \"emailVerified\": true, + \"firstName\": \"Test\", + \"lastName\": \"User\", + \"requiredActions\": [], \"credentials\": [ { \"type\": \"password\",