You are a senior code reviewer preparing to review code changes.

## Code Changes

```diff
diff --git a/expert_build/propose.py b/expert_build/propose.py
index d38db8a..113dfea 100644
--- a/expert_build/propose.py
+++ b/expert_build/propose.py
@@ -332,6 +332,9 @@ def cmd_propose_beliefs(args):
                 for line in content[3:end].splitlines():
                     if line.startswith("source_url:"):
                         source_url = line.split(":", 1)[1].strip()
+                    elif line.startswith("source:"):
+                        if not source_url:
+                            source_url = line.split(":", 1)[1].strip()
         header = f"--- FILE: {entry_path}"
         if source_url:
             header += f" | SOURCE_URL: {source_url}"
diff --git a/expert_build/summarize.py b/expert_build/summarize.py
index e0d6e0b..e88ef7a 100644
--- a/expert_build/summarize.py
+++ b/expert_build/summarize.py
@@ -1,8 +1,8 @@
 """Summarize source documents into entries using an LLM."""
 
 import re
-import subprocess
 import sys
+from datetime import date
 from pathlib import Path
 
 from .llm import check_model_available, invoke_sync
@@ -63,6 +63,9 @@ def cmd_summarize(args):
                 for line in frontmatter.splitlines():
                     if line.startswith("source_url:"):
                         source_url = line.split(":", 1)[1].strip()
+                    elif line.startswith("source:"):
+                        if not source_url:
+                            source_url = line.split(":", 1)[1].strip()
                     elif line.startswith("source_id:"):
                         source_id = line.split(":", 1)[1].strip()
                 content = content[end + 3:].strip()
@@ -96,41 +99,21 @@ def cmd_summarize(args):
         title = title_match.group(1) if title_match else source_path.stem.replace("-", " ").title()
         topic = source_path.stem
 
-        # Create entry via entry CLI
-        entry_path = None
-        try:
-            result = subprocess.run(
-                ["entry", "create", topic, title, "--content", summary],
-                capture_output=True, text=True,
-            )
-            if result.returncode == 0:
-                entry_path = result.stdout.strip().replace("Created ", "")
-                print(f"  -> {result.stdout.strip()}")
-            else:
-                # Try alternative invocation
-                result = subprocess.run(
-                    ["entry", "create", topic, title],
-                    input=summary,
-                    capture_output=True, text=True,
-                )
-                if result.returncode == 0:
-                    entry_path = result.stdout.strip().replace("Created ", "")
-                    print(f"  -> {result.stdout.strip()}")
-                else:
-                    print(f"  WARN: entry create failed: {result.stderr.strip()}")
-        except FileNotFoundError:
-            print("  ERROR: entry CLI not found. Install with: uv tool install entry")
-            sys.exit(1)
-
-        # Prepend source provenance frontmatter to the entry file
-        if entry_path and source_url:
-            ep = Path(entry_path)
-            if ep.exists():
-                fm = f"---\nsource_url: {source_url}\n"
-                if source_id:
-                    fm += f"source_id: {source_id}\n"
-                fm += "---\n\n"
-                ep.write_text(fm + ep.read_text())
+        # Write entry directly with provenance frontmatter
+        today = date.today()
+        entry_dir = Path("entries") / str(today.year) / f"{today.month:02d}" / f"{today.day:02d}"
+        entry_dir.mkdir(parents=True, exist_ok=True)
+        entry_path = entry_dir / f"{topic}.md"
+
+        fm_lines = [f"source: {source_path}"]
+        if source_url:
+            fm_lines.append(f"source_url: {source_url}")
+        if source_id:
+            fm_lines.append(f"source_id: {source_id}")
+        frontmatter = "---\n" + "\n".join(fm_lines) + "\n---\n\n"
+
+        entry_path.write_text(frontmatter + summary + "\n")
+        print(f"  -> Created {entry_path}")
 
         # Record as done
         with manifest.open("a") as f:
diff --git a/tests/test_summarize.py b/tests/test_summarize.py
index 5d4d0c1..ff3457d 100644
--- a/tests/test_summarize.py
+++ b/tests/test_summarize.py
@@ -2,7 +2,7 @@
 
 import types
 from pathlib import Path
-from unittest.mock import patch, MagicMock
+from unittest.mock import patch
 
 import pytest
 
@@ -33,6 +33,13 @@ def make_args(input_dir, model="test-model", limit=None):
     return types.SimpleNamespace(input_dir=str(input_dir), model=model, limit=limit)
 
 
+def _find_entry(work_dir):
+    """Find the generated entry file under entries/."""
+    entries = list((work_dir / "entries").rglob("*.md"))
+    assert len(entries) == 1, f"Expected 1 entry, found {len(entries)}: {entries}"
+    return entries[0]
+
+
 # --- File discovery tests ---
 
 def test_discovers_md_files(source_dir, work_dir):
@@ -40,12 +47,11 @@ def test_discovers_md_files(source_dir, work_dir):
     args = make_args(source_dir)
 
     with patch("expert_build.summarize.check_model_available", return_value=True), \
-         patch("expert_build.summarize.invoke_sync", return_value="## Topic Title\nSummary"), \
-         patch("subprocess.run") as mock_run:
-        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/doc.md", stderr="")
+         patch("expert_build.summarize.invoke_sync", return_value="## Topic Title\nSummary"):
         cmd_summarize(args)
 
-    assert mock_run.called
+    entry = _find_entry(work_dir)
+    assert "Summary" in entry.read_text()
 
 
 def test_discovers_py_files(source_dir, work_dir):
@@ -53,12 +59,11 @@ def test_discovers_py_files(source_dir, work_dir):
     args = make_args(source_dir)
 
     with patch("expert_build.summarize.check_model_available", return_value=True), \
-         patch("expert_build.summarize.invoke_sync", return_value="## Module\nSummary") as mock_llm, \
-         patch("subprocess.run") as mock_run:
-        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/module.md", stderr="")
+         patch("expert_build.summarize.invoke_sync", return_value="## Module\nSummary"):
         cmd_summarize(args)
 
-    assert mock_llm.called
+    entry = _find_entry(work_dir)
+    assert "Summary" in entry.read_text()
 
 
 def test_discovers_both_md_and_py(source_dir, work_dir):
@@ -66,14 +71,13 @@ def test_discovers_both_md_and_py(source_dir, work_dir):
     (source_dir / "beta.py").write_text("x = 1")
     args = make_args(source_dir)
 
-    calls = []
     with patch("expert_build.summarize.check_model_available", return_value=True), \
-         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary") as mock_llm, \
-         patch("subprocess.run") as mock_run:
-        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/x.md", stderr="")
+         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary") as mock_llm:
         cmd_summarize(args)
 
     assert mock_llm.call_count == 2
+    entries = list((work_dir / "entries").rglob("*.md"))
+    assert len(entries) == 2
 
 
 def test_ignores_other_extensions(source_dir, work_dir):
@@ -95,9 +99,7 @@ def test_uses_summarize_code_for_py(source_dir, work_dir):
     args = make_args(source_dir)
 
     with patch("expert_build.summarize.check_model_available", return_value=True), \
-         patch("expert_build.summarize.invoke_sync", return_value="## Module\nSummary") as mock_llm, \
-         patch("subprocess.run") as mock_run:
-        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/module.md", stderr="")
+         patch("expert_build.summarize.invoke_sync", return_value="## Module\nSummary") as mock_llm:
         cmd_summarize(args)
 
     prompt = mock_llm.call_args[0][0]
@@ -109,9 +111,7 @@ def test_uses_summarize_for_md(source_dir, work_dir):
     args = make_args(source_dir)
 
     with patch("expert_build.summarize.check_model_available", return_value=True), \
-         patch("expert_build.summarize.invoke_sync", return_value="## Doc Title\nSummary") as mock_llm, \
-         patch("subprocess.run") as mock_run:
-        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/doc.md", stderr="")
+         patch("expert_build.summarize.invoke_sync", return_value="## Doc Title\nSummary") as mock_llm:
         cmd_summarize(args)
 
     prompt = mock_llm.call_args[0][0]
@@ -125,9 +125,7 @@ def test_truncation_warning_for_large_file(source_dir, work_dir, capsys):
     args = make_args(source_dir)
 
     with patch("expert_build.summarize.check_model_available", return_value=True), \
-         patch("expert_build.summarize.invoke_sync", return_value="## Big Doc\nSummary") as mock_llm, \
-         patch("subprocess.run") as mock_run:
-        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/big.md", stderr="")
+         patch("expert_build.summarize.invoke_sync", return_value="## Big Doc\nSummary"):
         cmd_summarize(args)
 
     captured = capsys.readouterr()
@@ -140,9 +138,7 @@ def test_truncation_content_is_capped(source_dir, work_dir):
     args = make_args(source_dir)
 
     with patch("expert_build.summarize.check_model_available", return_value=True), \
-         patch("expert_build.summarize.invoke_sync", return_value="## Big\nSummary") as mock_llm, \
-         patch("subprocess.run") as mock_run:
-        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/big.md", stderr="")
+         patch("expert_build.summarize.invoke_sync", return_value="## Big\nSummary") as mock_llm:
         cmd_summarize(args)
 
     prompt = mock_llm.call_args[0][0]
@@ -155,9 +151,7 @@ def test_no_truncation_warning_for_small_file(source_dir, work_dir, capsys):
     args = make_args(source_dir)
 
     with patch("expert_build.summarize.check_model_available", return_value=True), \
-         patch("expert_build.summarize.invoke_sync", return_value="## Small\nSummary") as mock_llm, \
-         patch("subprocess.run") as mock_run:
-        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/small.md", stderr="")
+         patch("expert_build.summarize.invoke_sync", return_value="## Small\nSummary"):
         cmd_summarize(args)
 
     captured = capsys.readouterr()
@@ -184,9 +178,7 @@ def test_manifest_records_processed_file(source_dir, work_dir):
     args = make_args(source_dir)
 
     with patch("expert_build.summarize.check_model_available", return_value=True), \
-         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary"), \
-         patch("subprocess.run") as mock_run:
-        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/doc.md", stderr="")
+         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary"):
         cmd_summarize(args)
 
     manifest = work_dir / ".summarized"
@@ -197,14 +189,26 @@ def test_manifest_records_processed_file(source_dir, work_dir):
 # --- Frontmatter stripping tests ---
 
 def test_strips_frontmatter_before_summarizing(source_dir, work_dir):
+    content = "---\nsource: https://example.com\n---\n\nActual content here"
+    (source_dir / "doc.md").write_text(content)
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary") as mock_llm:
+        cmd_summarize(args)
+
+    prompt = mock_llm.call_args[0][0]
+    assert "source:" not in prompt
+    assert "Actual content here" in prompt
+
+
+def test_strips_source_url_frontmatter(source_dir, work_dir):
     content = "---\nsource_url: https://example.com\n---\n\nActual content here"
     (source_dir / "doc.md").write_text(content)
     args = make_args(source_dir)
 
     with patch("expert_build.summarize.check_model_available", return_value=True), \
-         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary") as mock_llm, \
-         patch("subprocess.run") as mock_run:
-        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/doc.md", stderr="")
+         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary") as mock_llm:
         cmd_summarize(args)
 
     prompt = mock_llm.call_args[0][0]
@@ -213,7 +217,7 @@ def test_strips_frontmatter_before_summarizing(source_dir, work_dir):
 
 
 def test_skips_empty_content_after_frontmatter(source_dir, work_dir, capsys):
-    (source_dir / "empty.md").write_text("---\nsource_url: https://example.com\n---\n\n")
+    (source_dir / "empty.md").write_text("---\nsource: https://example.com\n---\n\n")
     args = make_args(source_dir)
 
     with patch("expert_build.summarize.check_model_available", return_value=True), \
@@ -225,6 +229,83 @@ def test_skips_empty_content_after_frontmatter(source_dir, work_dir, capsys):
     assert "SKIP" in captured.out
 
 
+# --- Provenance frontmatter tests ---
+
+def test_entry_has_source_frontmatter(source_dir, work_dir):
+    """Generated entry includes source path in frontmatter."""
+    (source_dir / "doc.md").write_text("# Hello\nContent")
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary"):
+        cmd_summarize(args)
+
+    entry = _find_entry(work_dir)
+    content = entry.read_text()
+    assert content.startswith("---\n")
+    assert f"source: {source_dir}/doc.md" in content
+
+
+def test_entry_has_source_url_from_fetch_frontmatter(source_dir, work_dir):
+    """source: URL from fetch-docs frontmatter propagates as source_url."""
+    fm = "---\nsource: https://example.com/docs/page\nfetched: 2026-06-04\n---\n\nDoc content"
+    (source_dir / "page.md").write_text(fm)
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Page\nSummary"):
+        cmd_summarize(args)
+
+    entry = _find_entry(work_dir)
+    content = entry.read_text()
+    assert "source_url: https://example.com/docs/page" in content
+
+
+def test_entry_has_source_id_when_present(source_dir, work_dir):
+    """source_id propagates from source frontmatter to entry."""
+    fm = "---\nsource_url: https://example.com\nsource_id: abc123\n---\n\nContent"
+    (source_dir / "doc.md").write_text(fm)
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary"):
+        cmd_summarize(args)
+
+    entry = _find_entry(work_dir)
+    content = entry.read_text()
+    assert "source_url: https://example.com" in content
+    assert "source_id: abc123" in content
+
+
+def test_entry_contains_llm_summary(source_dir, work_dir):
+    """The LLM summary is written as the entry body."""
+    (source_dir / "doc.md").write_text("# Hello\nContent")
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## My Title\nDetailed summary here"):
+        cmd_summarize(args)
+
+    entry = _find_entry(work_dir)
+    content = entry.read_text()
+    assert "Detailed summary here" in content
+
+
+def test_entry_directory_structure(source_dir, work_dir):
+    """Entries are written to entries/YYYY/MM/DD/topic.md."""
+    (source_dir / "my-topic.md").write_text("# Topic\nContent")
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary"):
+        cmd_summarize(args)
+
+    entry = _find_entry(work_dir)
+    assert entry.name == "my-topic.md"
+    parts = entry.relative_to(work_dir).parts
+    assert parts[0] == "entries"
+
+
 # --- Prompt template tests ---
 
 def test_summarize_template_requests_descriptive_title():

```

## Your Task

Analyze the diff and identify what additional information you need to render confident verdicts.
Do NOT render verdicts yet. Only request observations.

## Available Observation Tools

| Tool | Purpose | When to use |
|------|---------|-------------|
| `exception_hierarchy` | Show exception MRO and subclasses | Retry logic, exception handling |
| `raises_analysis` | What exceptions a function raises | New function calls, error paths |
| `call_graph` | What a function calls | Impact analysis |
| `find_usages` | Where a symbol is used (with prod/test split) | Quick integration lookup |
| `find_callers` | Caller analysis with prod/test split and calling context | Method signature changes, return type changes, constructor modifications, integration verification |
| `test_coverage` | Find tests for a file (uses coverage-map if available) | Test coverage claims |
| `coverage_map_tests` | Find tests covering a file (from coverage-map.json) | Precise test coverage from actual execution |
| `coverage_map_files` | Find files covered by tests matching a pattern | Impact analysis for test changes |
| `function_body` | Full source of a function/method | Need complete function context beyond diff hunks |
| `file_imports` | Extract imports from a file | Verify import changes, check dependencies |
| `project_dependencies` | Get pyproject.toml/requirements.txt | Verify new imports have dependencies |
| `related_test_files` | Find test files for a source file | Discover tests by naming, imports, and coverage map |
| `class_hierarchy` | Show base classes and their `__init__` signatures | Class changes its parent, modifies `__init__`, or uses `super()` |
| `symbol_migration` | Check if a rename is complete across the repo | Symbol renamed in diff — verify old name is fully removed |
| `generator_info` | Report whether a function uses `yield` | Function might be a generator — affects return value semantics |

## What to Look For

1. **Exception handling**: Any `retry_if_exception_type`, `except`, or exception class references
2. **New dependencies**: Calls to external libraries where you don't know the error behavior
3. **Behavioral changes**: Modified logic where you need to verify callers/callees
4. **Test claims**: References to tests you can't see in the diff
5. **Inheritance changes**: Class definition changes, new base classes, `super()` calls
6. **Renames**: Symbols that appear to have been renamed in the diff
7. **Factory methods**: Calls to `@classmethod` / `@staticmethod` constructors (e.g. `Result.error(...)`) — request `function_body` to see their implementation

## Output Format

Output a JSON array of observation requests:

```json
[
  {"name": "descriptive_name", "tool": "tool_name", "params": {"param": "value"}},
  ...
]
```

If you don't need any observations (simple changes, all context is in the diff), output:

```json
[]
```

## Examples

For a diff containing `retry_if_exception_type((OSError, httpx.TransportError))`:
```json
[
  {"name": "oserror_subclasses", "tool": "exception_hierarchy", "params": {"class_name": "builtins.OSError"}},
  {"name": "transport_errors", "tool": "exception_hierarchy", "params": {"class_name": "httpx.TransportError"}}
]
```

For a diff adding a new function that calls `oauth_client.get_access_token()`:
```json
[
  {"name": "oauth_exceptions", "tool": "raises_analysis", "params": {"file_path": "src/auth/oauth.py", "function_name": "get_access_token"}}
]
```

For a diff modifying a method but you need the full function to verify:
```json
[
  {"name": "full_getattr", "tool": "function_body", "params": {"file_path": "src/proxy.py", "function_name": "__getattr__"}}
]
```

For a diff changing a method signature or return type (verify all callers):
```json
[
  {"name": "handle_request_callers", "tool": "find_callers", "params": {"symbol": "handle_request"}}
]
```

For a diff adding new imports (e.g., `import httpx`):
```json
[
  {"name": "file_imports", "tool": "file_imports", "params": {"file_path": "src/client.py"}},
  {"name": "project_deps", "tool": "project_dependencies", "params": {}}
]
```

For a diff calling a factory method like `ModuleResult.error_result(msg)`:
```json
[
  {"name": "error_result_body", "tool": "function_body", "params": {"file_path": "src/models.py", "function_name": "error_result"}}
]
```

For a diff where a class changes its parent class:
```json
[
  {"name": "client_hierarchy", "tool": "class_hierarchy", "params": {"class_name": "MyClient", "file_path": "src/client.py"}}
]
```

For a diff that renames a symbol (e.g., `OldClient` to `NewClient`):
```json
[
  {"name": "client_rename", "tool": "symbol_migration", "params": {"old_name": "OldClient", "new_name": "NewClient"}}
]
```

For a diff modifying a function that might be a generator:
```json
[
  {"name": "process_gen", "tool": "generator_info", "params": {"file_path": "src/pipeline.py", "function_name": "process_items"}}
]
```

Now analyze the diff above and output your observation requests as JSON:
