From cab82d92067b26d2a5d3394ae096b770cfd95ce4 Mon Sep 17 00:00:00 2001 From: Tanner Date: Tue, 31 Mar 2026 09:43:15 -0400 Subject: [PATCH 1/2] feat: add system prompt injection for memory MCP instructions Add build_system_prompt() to claw/core/agent.py and pass it via the system_prompt field on ClaudeAgentOptions in create_client(). Also add CLAUDE.md at repo root as belt-and-suspenders (picked up via cwd). Instructs Claude to use read_memory()/write_memory() from the claw-memory MCP server and warns against local filesystem persistence. Closes #68 Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 22 ++++++ claw/core/agent.py | 27 +++++++ openspec/changes/system-prompt/proposal.md | 18 +++++ .../system-prompt/specs/system-prompt/spec.md | 36 ++++++++++ openspec/changes/system-prompt/tasks.md | 7 ++ tests/test_agent.py | 72 +++++++++++++++++++ 6 files changed, 182 insertions(+) create mode 100644 CLAUDE.md create mode 100644 openspec/changes/system-prompt/proposal.md create mode 100644 openspec/changes/system-prompt/specs/system-prompt/spec.md create mode 100644 openspec/changes/system-prompt/tasks.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d9f9065 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,22 @@ +# Claw — Databricks Personal Assistant + +## Persistent Memory + +You have access to persistent memory via the claw-memory MCP server. + +At the START of each conversation: +- Call read_memory() to load your persistent context + +When the user shares important information: +- Call write_memory(content) to update persistent memory +- Include ALL existing memory plus new additions (full replace, not append) +- Use structured sections with headers +- Remember: names, preferences, ongoing projects, decisions made + +NEVER write files to the local filesystem for persistence — the container is +ephemeral. Use write_memory() via the memory MCP only. + +## Behavior +- Be concise and helpful +- You have access to Databricks tools via MCP servers +- All tool calls are pre-approved (bypassPermissions mode) diff --git a/claw/core/agent.py b/claw/core/agent.py index 9147987..d2e7a9e 100644 --- a/claw/core/agent.py +++ b/claw/core/agent.py @@ -68,6 +68,32 @@ def preflight_check_cli(config: Any) -> None: logger.error("preflight_check_cli failed: %s", exc) +def build_system_prompt(config: Any) -> str: + """Build the system prompt for Claude — instructions for memory and behavior.""" + return """You are Claw, a personal assistant running on Databricks. + +## Persistent Memory +You have access to persistent memory via the claw-memory MCP server. + +At the START of each conversation: +- Call read_memory() to load your persistent context + +When the user shares important information: +- Call write_memory(content) to update persistent memory +- Include ALL existing memory plus new additions (full replace, not append) +- Use structured sections with headers +- Remember: names, preferences, ongoing projects, decisions made + +NEVER write files to the local filesystem for persistence — the container is \ +ephemeral. Use write_memory() via the memory MCP only. + +## Behavior +- Be concise and helpful +- You have access to Databricks tools via MCP servers +- Use bypassPermissions mode — all tool calls are pre-approved +""" + + def build_sdk_env(config: Any) -> dict[str, str]: """Build the sdk_env dict for the Claude Agent SDK. @@ -100,6 +126,7 @@ async def create_client( options = ClaudeAgentOptions( resume=sdk_session_id, permission_mode="bypassPermissions", + system_prompt=build_system_prompt(config), env=build_sdk_env(config), mcp_servers=mcp_servers, cwd=cwd, diff --git a/openspec/changes/system-prompt/proposal.md b/openspec/changes/system-prompt/proposal.md new file mode 100644 index 0000000..17fb957 --- /dev/null +++ b/openspec/changes/system-prompt/proposal.md @@ -0,0 +1,18 @@ +# System Prompt Injection — tell Claude about memory MCP + +## Problem +Claude has no instructions about the memory MCP server or persistent storage. +Memory context is injected as a prompt prefix in slack_poller.py, but Claude +doesn't know to *write* memory or use the MCP tools proactively. + +## Solution +1. Add `build_system_prompt(config)` to `claw/core/agent.py` — returns a string + instructing Claude to use read_memory/write_memory from the claw-memory MCP. +2. Pass it via the `system_prompt` field on `ClaudeAgentOptions` in `create_client()`. +3. Add a `CLAUDE.md` at repo root (belt-and-suspenders) with the same memory + instructions, picked up automatically since `cwd` is already set to repo root. + +## Files changed +- `claw/core/agent.py` — add `build_system_prompt()`, wire into `create_client()` +- `CLAUDE.md` (new) — memory instructions for Claude Code cwd discovery +- `tests/test_agent.py` — tests for `build_system_prompt()` and system_prompt in options diff --git a/openspec/changes/system-prompt/specs/system-prompt/spec.md b/openspec/changes/system-prompt/specs/system-prompt/spec.md new file mode 100644 index 0000000..f867e5c --- /dev/null +++ b/openspec/changes/system-prompt/specs/system-prompt/spec.md @@ -0,0 +1,36 @@ +# Spec: system-prompt + +## Module +`claw.core.agent` + +## Public API + +### build_system_prompt(config: Any) -> str +Returns a non-empty system prompt string that: +- Contains "read_memory" instruction +- Contains "write_memory" instruction +- Warns against local filesystem persistence +- Mentions structured sections for memory content + +### create_client changes +- `ClaudeAgentOptions` now includes `system_prompt=build_system_prompt(config)` +- All other existing fields unchanged + +## CLAUDE.md (repo root) +Contains memory MCP instructions. Discovered automatically via `cwd=repo_root`. + +## Tests + +### TestBuildSystemPrompt +1. `test_returns_nonempty_string` — result is a non-empty str +2. `test_contains_read_memory` — "read_memory" in result +3. `test_contains_write_memory` — "write_memory" in result +4. `test_warns_no_filesystem_persistence` — "filesystem" or "disk" in result +5. `test_mentions_structured_sections` — "structured" in result + +### TestCreateClient (additions) +6. `test_system_prompt_passed_in_options` — options.system_prompt is non-empty string + +### TestClaudeMd +7. `test_claude_md_exists` — CLAUDE.md file exists at repo root +8. `test_claude_md_contains_memory_instructions` — file contains "read_memory" and "write_memory" diff --git a/openspec/changes/system-prompt/tasks.md b/openspec/changes/system-prompt/tasks.md new file mode 100644 index 0000000..07223bd --- /dev/null +++ b/openspec/changes/system-prompt/tasks.md @@ -0,0 +1,7 @@ +# Tasks + +- [x] OpenSpec written +- [ ] RED: Write failing tests for build_system_prompt and system_prompt in options +- [ ] GREEN: Implement build_system_prompt(), wire into create_client(), create CLAUDE.md +- [ ] All tests pass (266 existing + new) +- [ ] Commit and PR diff --git a/tests/test_agent.py b/tests/test_agent.py index cc1ab05..1580001 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -543,3 +543,75 @@ async def _fake_receive(): pass assert gen.progress_event_received is False + + +# --------------------------------------------------------------------------- +# Issue #68: build_system_prompt and system_prompt in create_client +# --------------------------------------------------------------------------- + + +class TestBuildSystemPrompt: + def test_returns_nonempty_string(self): + from claw.core.agent import build_system_prompt + + result = build_system_prompt(_fake_config()) + assert isinstance(result, str) + assert len(result) > 0 + + def test_contains_read_memory(self): + from claw.core.agent import build_system_prompt + + result = build_system_prompt(_fake_config()) + assert "read_memory" in result + + def test_contains_write_memory(self): + from claw.core.agent import build_system_prompt + + result = build_system_prompt(_fake_config()) + assert "write_memory" in result + + def test_warns_no_filesystem_persistence(self): + from claw.core.agent import build_system_prompt + + result = build_system_prompt(_fake_config()) + # Must warn against using local filesystem for persistence + assert "filesystem" in result.lower() or "disk" in result.lower() + + def test_mentions_structured_sections(self): + from claw.core.agent import build_system_prompt + + result = build_system_prompt(_fake_config()) + assert "structured" in result.lower() + + +class TestSystemPromptInCreateClient: + @pytest.mark.asyncio + async def test_system_prompt_passed_in_options(self): + from claw.core.agent import create_client + + mock_client_instance = MagicMock() + mock_client_instance.connect = AsyncMock() + + with patch("claw.core.agent.ClaudeSDKClient", return_value=mock_client_instance) as mock_cls: + await create_client(_fake_config(), {}) + + call_kwargs = mock_cls.call_args + options = call_kwargs[1]["options"] if "options" in (call_kwargs[1] or {}) else call_kwargs[0][0] + assert options.system_prompt is not None + assert isinstance(options.system_prompt, str) + assert len(options.system_prompt) > 0 + assert "read_memory" in options.system_prompt + + +class TestClaudeMd: + def test_claude_md_exists(self): + repo_root = Path(__file__).resolve().parents[1] + claude_md = repo_root / "CLAUDE.md" + assert claude_md.exists(), f"CLAUDE.md not found at {claude_md}" + + def test_claude_md_contains_memory_instructions(self): + repo_root = Path(__file__).resolve().parents[1] + claude_md = repo_root / "CLAUDE.md" + content = claude_md.read_text() + assert "read_memory" in content + assert "write_memory" in content From f996bd2d39b32b21997f90bf1a863aa7b5c869cc Mon Sep 17 00:00:00 2001 From: Tanner Date: Tue, 31 Mar 2026 09:45:52 -0400 Subject: [PATCH 2/2] fix: use SystemPromptPreset append= to preserve default Claude Code system prompt Replaces plain string system_prompt with SystemPromptPreset(type="preset", preset="claude_code", append=...) so skills, MCP descriptions, and default Claude Code behavior are preserved. Memory instructions are appended only. Co-Authored-By: Claude Sonnet 4.6 --- claw/core/agent.py | 22 +++++++++++----------- tests/test_agent.py | 26 +++++++++++++++----------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/claw/core/agent.py b/claw/core/agent.py index d2e7a9e..c473b2f 100644 --- a/claw/core/agent.py +++ b/claw/core/agent.py @@ -19,6 +19,7 @@ ClaudeAgentOptions, ResultMessage, StreamEvent, + SystemPromptPreset, TaskProgressMessage, TaskStartedMessage, ) @@ -68,11 +69,15 @@ def preflight_check_cli(config: Any) -> None: logger.error("preflight_check_cli failed: %s", exc) -def build_system_prompt(config: Any) -> str: - """Build the system prompt for Claude — instructions for memory and behavior.""" - return """You are Claw, a personal assistant running on Databricks. +def build_system_prompt(config: Any) -> SystemPromptPreset: + """Build a system prompt preset that appends memory instructions to Claude's default prompt. -## Persistent Memory + Uses SystemPromptPreset with preset="claude_code" so skills, MCP descriptions, + and default Claude Code behavior are preserved. Only the memory/persistence + instructions are appended. + """ + append_text = """ +## Persistent Memory (Claw) You have access to persistent memory via the claw-memory MCP server. At the START of each conversation: @@ -84,14 +89,9 @@ def build_system_prompt(config: Any) -> str: - Use structured sections with headers - Remember: names, preferences, ongoing projects, decisions made -NEVER write files to the local filesystem for persistence — the container is \ -ephemeral. Use write_memory() via the memory MCP only. - -## Behavior -- Be concise and helpful -- You have access to Databricks tools via MCP servers -- Use bypassPermissions mode — all tool calls are pre-approved +NEVER write files to the local filesystem for persistence — the container is ephemeral. Use write_memory() via the memory MCP only. """ + return SystemPromptPreset(type="preset", preset="claude_code", append=append_text) def build_sdk_env(config: Any) -> dict[str, str]: diff --git a/tests/test_agent.py b/tests/test_agent.py index 1580001..e9d4d5a 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -551,37 +551,39 @@ async def _fake_receive(): class TestBuildSystemPrompt: - def test_returns_nonempty_string(self): + def test_returns_system_prompt_preset(self): + from claude_agent_sdk.types import SystemPromptPreset + from claw.core.agent import build_system_prompt result = build_system_prompt(_fake_config()) - assert isinstance(result, str) - assert len(result) > 0 + assert isinstance(result, dict) + assert result["type"] == "preset" + assert result["preset"] == "claude_code" def test_contains_read_memory(self): from claw.core.agent import build_system_prompt result = build_system_prompt(_fake_config()) - assert "read_memory" in result + assert "read_memory" in result["append"] def test_contains_write_memory(self): from claw.core.agent import build_system_prompt result = build_system_prompt(_fake_config()) - assert "write_memory" in result + assert "write_memory" in result["append"] def test_warns_no_filesystem_persistence(self): from claw.core.agent import build_system_prompt result = build_system_prompt(_fake_config()) - # Must warn against using local filesystem for persistence - assert "filesystem" in result.lower() or "disk" in result.lower() + assert "filesystem" in result["append"].lower() or "disk" in result["append"].lower() def test_mentions_structured_sections(self): from claw.core.agent import build_system_prompt result = build_system_prompt(_fake_config()) - assert "structured" in result.lower() + assert "structured" in result["append"].lower() class TestSystemPromptInCreateClient: @@ -598,9 +600,11 @@ async def test_system_prompt_passed_in_options(self): call_kwargs = mock_cls.call_args options = call_kwargs[1]["options"] if "options" in (call_kwargs[1] or {}) else call_kwargs[0][0] assert options.system_prompt is not None - assert isinstance(options.system_prompt, str) - assert len(options.system_prompt) > 0 - assert "read_memory" in options.system_prompt + # Should be a SystemPromptPreset dict that appends, not a plain str + assert isinstance(options.system_prompt, dict) + assert options.system_prompt.get("preset") == "claude_code" + assert "append" in options.system_prompt + assert "read_memory" in options.system_prompt["append"] class TestClaudeMd: