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
22 changes: 22 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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)
27 changes: 27 additions & 0 deletions claw/core/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ClaudeAgentOptions,
ResultMessage,
StreamEvent,
SystemPromptPreset,
TaskProgressMessage,
TaskStartedMessage,
)
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions openspec/changes/system-prompt/proposal.md
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions openspec/changes/system-prompt/specs/system-prompt/spec.md
Original file line number Diff line number Diff line change
@@ -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"
7 changes: 7 additions & 0 deletions openspec/changes/system-prompt/tasks.md
Original file line number Diff line number Diff line change
@@ -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
76 changes: 76 additions & 0 deletions tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading