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..c473b2f 100644 --- a/claw/core/agent.py +++ b/claw/core/agent.py @@ -19,6 +19,7 @@ ClaudeAgentOptions, ResultMessage, StreamEvent, + SystemPromptPreset, TaskProgressMessage, TaskStartedMessage, ) @@ -68,6 +69,31 @@ def preflight_check_cli(config: Any) -> None: logger.error("preflight_check_cli failed: %s", exc) +def build_system_prompt(config: Any) -> SystemPromptPreset: + """Build a system prompt preset that appends memory instructions to Claude's default prompt. + + 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: +- 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. +""" + return SystemPromptPreset(type="preset", preset="claude_code", append=append_text) + + 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..e9d4d5a 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -543,3 +543,79 @@ 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_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, 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["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["append"] + + def test_warns_no_filesystem_persistence(self): + from claw.core.agent import build_system_prompt + + result = build_system_prompt(_fake_config()) + 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["append"].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 + # 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: + 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