mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(grok): deliver the role blueprint as grok's system prompt via ~/.grok/AGENTS.md
The blueprint was mounted at /app/system-prompt.md but never reached grok — a real
parity gap vs the Claude path (which passes --system-prompt-file). grok agents ran
only on the per-task prompt, missing their RoboCo role/org context.
Verified live on grok 0.2.56 that the obvious flags do NOT work headless:
`--system-prompt-override` and `--rules` are silently ignored under `grok -p`
(identical output with and without). What IS honoured is grok's instruction-file
discovery — and `$HOME/.grok/AGENTS.md` is loaded GLOBALLY regardless of --cwd
(a project AGENTS.md only loads from the cwd/project root, which would pollute the
agent's git workspace). Proven end to end: a blueprint written there makes grok
adopt the role ("I am the RoboCo intake interviewer ... -- intake-1").
write_agents_md() copies /app/system-prompt.md -> ~/.grok/AGENTS.md; the one-shot
render (grok_cli_config.main) and both interactive mains call it. No git pollution
(it lives in ~/.grok, not the workspace), and it covers repo-cwd and /app-cwd
roles alike. Reverted the non-working --system-prompt-override wiring.
This commit is contained in:
@@ -36,6 +36,10 @@ fi
|
|||||||
RUN_LOG="/tmp/grok-run.json"
|
RUN_LOG="/tmp/grok-run.json"
|
||||||
ERR_LOG="/tmp/grok-run.err"
|
ERR_LOG="/tmp/grok-run.err"
|
||||||
WORKSPACE="${ROBOCO_WORKSPACE:-$PWD}"
|
WORKSPACE="${ROBOCO_WORKSPACE:-$PWD}"
|
||||||
|
# The role blueprint reaches grok as its system prompt via ~/.grok/AGENTS.md (a
|
||||||
|
# global instruction file grok loads regardless of --cwd, verified live — the
|
||||||
|
# `--system-prompt-override`/`--rules` flags are ignored in headless mode). The
|
||||||
|
# render step above (grok_cli_config) wrote it from the mounted system prompt.
|
||||||
# NOTE: grok generates its own session id and ignores a requested one (`-s` does
|
# NOTE: grok generates its own session id and ignores a requested one (`-s` does
|
||||||
# not pin it), so we do NOT pass a session id in; usage capture below reads the
|
# not pin it), so we do NOT pass a session id in; usage capture below reads the
|
||||||
# real id back out of the JSON run log instead.
|
# real id back out of the JSON run log instead.
|
||||||
|
|||||||
@@ -193,6 +193,9 @@ class GrokCliSession: # pragma: no cover - needs the live grok binary
|
|||||||
*self._role_args,
|
*self._role_args,
|
||||||
*self._extra_args,
|
*self._extra_args,
|
||||||
]
|
]
|
||||||
|
# The role blueprint reaches grok as its system prompt via the global
|
||||||
|
# ~/.grok/AGENTS.md the main writes (not a per-turn flag — the headless
|
||||||
|
# --system-prompt-override is ignored).
|
||||||
if self._session_id:
|
if self._session_id:
|
||||||
argv += ["-r", self._session_id]
|
argv += ["-r", self._session_id]
|
||||||
return argv
|
return argv
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ from roboco.agent_sdk.intake_main import (
|
|||||||
make_message_source,
|
make_message_source,
|
||||||
make_relay_sink,
|
make_relay_sink,
|
||||||
)
|
)
|
||||||
from roboco.llm.providers.grok_cli_config import GROK_CONFIG_PATH, render_config_toml
|
from roboco.llm.providers.grok_cli_config import (
|
||||||
|
GROK_CONFIG_PATH,
|
||||||
|
render_config_toml,
|
||||||
|
write_agents_md,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
@@ -80,6 +84,8 @@ async def main() -> None: # pragma: no cover - needs the live container + grok
|
|||||||
cwd = os.environ.get("ROBOCO_WORKSPACE", "/data/workspace")
|
cwd = os.environ.get("ROBOCO_WORKSPACE", "/data/workspace")
|
||||||
|
|
||||||
_render_grok_config(base_url, session_id)
|
_render_grok_config(base_url, session_id)
|
||||||
|
# Install the role blueprint as grok's global system prompt (~/.grok/AGENTS.md).
|
||||||
|
write_agents_md()
|
||||||
|
|
||||||
queue: asyncio.Queue[str | None] = asyncio.Queue()
|
queue: asyncio.Queue[str | None] = asyncio.Queue()
|
||||||
client = httpx.AsyncClient(timeout=30.0)
|
client = httpx.AsyncClient(timeout=30.0)
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ from roboco.agent_sdk.grok_cli_session import GrokCliSession
|
|||||||
from roboco.agent_sdk.intake_driver import IntakeDriver
|
from roboco.agent_sdk.intake_driver import IntakeDriver
|
||||||
from roboco.agent_sdk.intake_main import build_receiver, make_message_source
|
from roboco.agent_sdk.intake_main import build_receiver, make_message_source
|
||||||
from roboco.agent_sdk.secretary_main import make_relay_sink
|
from roboco.agent_sdk.secretary_main import make_relay_sink
|
||||||
from roboco.llm.providers.grok_cli_config import GROK_CONFIG_PATH, render_config_toml
|
from roboco.llm.providers.grok_cli_config import (
|
||||||
|
GROK_CONFIG_PATH,
|
||||||
|
render_config_toml,
|
||||||
|
write_agents_md,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
@@ -78,6 +82,8 @@ async def main() -> None: # pragma: no cover - needs the live container + grok
|
|||||||
cwd = os.environ.get("ROBOCO_WORKSPACE", "/app")
|
cwd = os.environ.get("ROBOCO_WORKSPACE", "/app")
|
||||||
|
|
||||||
_render_grok_config(base_url)
|
_render_grok_config(base_url)
|
||||||
|
# Install the role blueprint as grok's global system prompt (~/.grok/AGENTS.md).
|
||||||
|
write_agents_md()
|
||||||
|
|
||||||
queue: asyncio.Queue[str | None] = asyncio.Queue()
|
queue: asyncio.Queue[str | None] = asyncio.Queue()
|
||||||
client = httpx.AsyncClient(timeout=30.0)
|
client = httpx.AsyncClient(timeout=30.0)
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ from roboco.services.gateway.role_config import get_role_config
|
|||||||
# grok reads its global config from ``$HOME/.grok/config.toml`` (the agent's HOME
|
# grok reads its global config from ``$HOME/.grok/config.toml`` (the agent's HOME
|
||||||
# is ``/home/agent``; the host ``auth.json`` is mounted alongside it).
|
# is ``/home/agent``; the host ``auth.json`` is mounted alongside it).
|
||||||
GROK_CONFIG_PATH = Path.home() / ".grok" / "config.toml"
|
GROK_CONFIG_PATH = Path.home() / ".grok" / "config.toml"
|
||||||
|
# grok loads ``$HOME/.grok/AGENTS.md`` as a GLOBAL instruction file regardless of
|
||||||
|
# --cwd (verified live; the --system-prompt-override / --rules flags are ignored
|
||||||
|
# in headless mode). This is how the RoboCo role blueprint becomes grok's system
|
||||||
|
# prompt — the parity analogue of the Claude path's --system-prompt-file — without
|
||||||
|
# writing into (and polluting) the agent's git workspace.
|
||||||
|
GROK_AGENTS_PATH = Path.home() / ".grok" / "AGENTS.md"
|
||||||
|
# The composed role blueprint the orchestrator mounts into every agent container.
|
||||||
|
SYSTEM_PROMPT_PATH = Path(
|
||||||
|
os.environ.get("ROBOCO_SYSTEM_PROMPT", "/app/system-prompt.md")
|
||||||
|
)
|
||||||
# The entrypoint reads the computed flags (one token per line) from this file.
|
# The entrypoint reads the computed flags (one token per line) from this file.
|
||||||
GROK_ARGS_PATH = Path(os.environ.get("ROBOCO_GROK_ARGS_FILE", "/tmp/roboco-grok-args"))
|
GROK_ARGS_PATH = Path(os.environ.get("ROBOCO_GROK_ARGS_FILE", "/tmp/roboco-grok-args"))
|
||||||
|
|
||||||
@@ -178,8 +188,27 @@ def _load_mcp_config(path: str) -> dict[str, Any]:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def write_agents_md(
|
||||||
|
*, source: Path = SYSTEM_PROMPT_PATH, dest: Path = GROK_AGENTS_PATH
|
||||||
|
) -> bool:
|
||||||
|
"""Install the mounted role blueprint as grok's global system prompt.
|
||||||
|
|
||||||
|
Copies the composed prompt to ``~/.grok/AGENTS.md`` (the global instruction
|
||||||
|
file grok honours in headless mode). Best-effort: returns False and writes
|
||||||
|
nothing if the source is absent / unreadable, so a missing prompt never fails
|
||||||
|
the render.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
blueprint = source.read_text(encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
dest.write_text(blueprint, encoding="utf-8")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
"""Entrypoint: write ``~/.grok/config.toml`` + the per-role args file."""
|
"""Entrypoint: write ``~/.grok/config.toml`` + AGENTS.md + the per-role args."""
|
||||||
agent_id = os.environ.get("ROBOCO_AGENT_ID", "")
|
agent_id = os.environ.get("ROBOCO_AGENT_ID", "")
|
||||||
mcp_path = os.environ.get("ROBOCO_MCP_CONFIG", "/app/mcp-config.json")
|
mcp_path = os.environ.get("ROBOCO_MCP_CONFIG", "/app/mcp-config.json")
|
||||||
try:
|
try:
|
||||||
@@ -193,6 +222,7 @@ def main() -> int:
|
|||||||
GROK_CONFIG_PATH.write_text(
|
GROK_CONFIG_PATH.write_text(
|
||||||
render_config_toml(_load_mcp_config(mcp_path)), encoding="utf-8"
|
render_config_toml(_load_mcp_config(mcp_path)), encoding="utf-8"
|
||||||
)
|
)
|
||||||
|
write_agents_md()
|
||||||
GROK_ARGS_PATH.write_text(
|
GROK_ARGS_PATH.write_text(
|
||||||
"\n".join(grok_cli_args(agent_id, max_turns=max_turns)) + "\n", encoding="utf-8"
|
"\n".join(grok_cli_args(agent_id, max_turns=max_turns)) + "\n", encoding="utf-8"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from typing import TYPE_CHECKING
|
|||||||
from roboco.llm.providers import grok_cli_config as gc
|
from roboco.llm.providers import grok_cli_config as gc
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
_SAMPLE_MCP = {
|
_SAMPLE_MCP = {
|
||||||
@@ -42,6 +44,20 @@ def test_render_config_toml_empty_when_no_servers() -> None:
|
|||||||
assert gc.render_config_toml({"mcpServers": {}}) == ""
|
assert gc.render_config_toml({"mcpServers": {}}) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_agents_md_installs_the_blueprint(tmp_path: Path) -> None:
|
||||||
|
src = tmp_path / "system-prompt.md"
|
||||||
|
src.write_text("You are the RoboCo intake interviewer.", encoding="utf-8")
|
||||||
|
dest = tmp_path / ".grok" / "AGENTS.md"
|
||||||
|
assert gc.write_agents_md(source=src, dest=dest) is True
|
||||||
|
assert dest.read_text(encoding="utf-8") == "You are the RoboCo intake interviewer."
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_agents_md_noops_when_source_absent(tmp_path: Path) -> None:
|
||||||
|
dest = tmp_path / ".grok" / "AGENTS.md"
|
||||||
|
assert gc.write_agents_md(source=tmp_path / "absent.md", dest=dest) is False
|
||||||
|
assert not dest.exists()
|
||||||
|
|
||||||
|
|
||||||
def test_developer_flags(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_developer_flags(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.delenv("ROBOCO_GROK_REASONING_EFFORT", raising=False)
|
monkeypatch.delenv("ROBOCO_GROK_REASONING_EFFORT", raising=False)
|
||||||
args = gc.grok_cli_args("be-dev-1")
|
args = gc.grok_cli_args("be-dev-1")
|
||||||
|
|||||||
Reference in New Issue
Block a user