You are a senior code reviewer. Review the following code changes.

## Specification

No specification provided. Focus on correctness, tests, and integration.



## Issue Description

Review the code changes against this issue. Determine whether the changes address the problem or feature request described.

```markdown
## summary command fails on large belief networks (10k+ nodes)

## Problem

`project-expert summary` (called during `update`) fails on large networks:

```
Summarizing 10822 beliefs with claude...
Error: Model claude failed: 
WARN: summary failed (exit 1), continuing...
```

The error message is `Model claude failed:` with empty stderr -- Claude exited non-zero without error output.

## Root cause

In `cli.py:summary()` (line 1999), the command reads all beliefs via `reasons list` (line 2018) and passes the full output as `beliefs_text` into `build_summary_prompt()`. With 10,822 beliefs, the `reasons list` output is enormous -- likely exceeding Claude CLI input limits or the model context window.

The call chain:
1. `summary()` runs `reasons list` and captures all 10,822 belief lines
2. Passes full text to `build_summary_prompt()` (line 2037)
3. `invoke(prompt, model)` runs `claude -p` with the massive prompt
4. Claude exits non-zero (likely input too large), `invoke()` raises `RuntimeError` at `llm.py:49`
5. `cli.py:2045-2047` catches and exits with code 1

## Observed on

- Network size: 10,822 beliefs
- Model: `claude` (default)
- Called via: `project-expert update --since-last --all-pages`

## Suggested fix

1. **Truncate or sample beliefs for summary** -- the summary does not need all 10,822 beliefs. Sample the top N by recency, depth, or topic diversity.
2. **Use `reasons list` with filters** -- only pass IN beliefs, or only premises, or beliefs above a certain depth.
3. **Chunk and summarize** -- split beliefs into chunks, summarize each, then synthesize.
4. **Report the actual failure clearly** -- the `RuntimeError` has empty stderr, making it hard to diagnose. Log the prompt size and any subprocess output.
```


## Code Changes

```diff
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..7c06a91
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,26 @@
+# CLAUDE.md
+
+## Project
+
+`ftl-project-expert` — CLI tool that builds expert knowledge bases from project management data (GitHub, GitLab, Jira). Uses LLMs to scan issues, explore topics, extract beliefs, and derive new insights.
+
+## Dev Setup
+
+```bash
+uv sync
+```
+
+## Issue Fix Process
+
+When fixing a GitHub issue, follow this workflow:
+
+1. **Read the issue** — `gh issue view <number> --repo benthomasson/ftl-project-expert --comments`
+2. **Checkout main and pull latest** — `git checkout main && git pull`
+3. **Fix on a dedicated branch from main** — branch name should reflect the fix (e.g. `fix-export-json-corruption`)
+4. **Commit and push**, then open a PR referencing the issue (`Fixes #N`)
+5. **Run code review with comment** — `code-review review-loop --pr <url> --github-issue <issue-url> --comment`
+6. **Address review concerns** — commit and push follow-up fixes
+7. **Re-review** until concerns are resolved or acknowledged as out-of-scope
+8. **Squash-merge** — `gh pr merge <number> --squash --delete-branch`
+9. **Checkout main and pull latest** — `git checkout main && git pull`
+10. **Reinstall uv tool** — `uv tool install --reinstall -e .`
diff --git a/ftl_project_expert/cli.py b/ftl_project_expert/cli.py
index 9387992..570125b 100644
--- a/ftl_project_expert/cli.py
+++ b/ftl_project_expert/cli.py
@@ -2031,17 +2031,34 @@ def summary(ctx):
         sys.exit(1)
 
     # Read beliefs from reasons or beliefs.md
+    max_beliefs = 500
     beliefs_text = ""
     belief_count = 0
+    total_count = 0
+    sorted_by_impact = False
 
     if _has_reasons() and Path("reasons.db").exists():
-        result = subprocess.run(["reasons", "list"], capture_output=True, text=True)
+        result = subprocess.run(
+            ["reasons", "list", "--status", "IN", "--by-impact"],
+            capture_output=True, text=True,
+        )
         if result.returncode == 0 and result.stdout.strip():
-            beliefs_text = result.stdout
-            belief_count = len([l for l in result.stdout.splitlines() if l.strip()])
+            lines = [l for l in result.stdout.splitlines() if l.strip()]
+            total_count = len(lines)
+            if total_count > max_beliefs:
+                lines = lines[:max_beliefs]
+            beliefs_text = "\n".join(lines)
+            belief_count = len(lines)
+            sorted_by_impact = True
     elif Path("beliefs.md").exists():
-        beliefs_text = Path("beliefs.md").read_text()
-        belief_count = len(re.findall(r"^### \S+", beliefs_text, re.MULTILINE))
+        full_text = Path("beliefs.md").read_text()
+        sections = re.split(r"(?=^### \S+)", full_text, flags=re.MULTILINE)
+        sections = [s for s in sections if s.strip()]
+        total_count = len(sections)
+        if total_count > max_beliefs:
+            sections = sections[:max_beliefs]
+        beliefs_text = "\n".join(sections)
+        belief_count = len(sections)
 
     if not beliefs_text or belief_count == 0:
         click.echo("No beliefs found. Run the pipeline first:")
@@ -2050,7 +2067,14 @@ def summary(ctx):
         click.echo("  project-expert accept-beliefs")
         sys.exit(1)
 
-    click.echo(f"Summarizing {belief_count} beliefs with {model}...", err=True)
+    if total_count > max_beliefs:
+        order = "by impact" if sorted_by_impact else "by file order"
+        click.echo(
+            f"Summarizing top {belief_count} of {total_count} beliefs ({order}) with {model}...",
+            err=True,
+        )
+    else:
+        click.echo(f"Summarizing {belief_count} beliefs with {model}...", err=True)
 
     project_name = config.get("repo", config.get("project", "unknown"))
 
@@ -2058,12 +2082,17 @@ def summary(ctx):
         beliefs_text=beliefs_text,
         project_name=project_name,
         belief_count=belief_count,
+        total_count=total_count,
     )
 
+    prompt_size_kb = len(prompt.encode()) / 1024
     try:
         result = asyncio.run(invoke(prompt, model, timeout=timeout))
     except Exception as e:
-        click.echo(f"Error: {e}", err=True)
+        click.echo(
+            f"Error: Model {model} failed (prompt size: {prompt_size_kb:.0f} KB): {e}",
+            err=True,
+        )
         sys.exit(1)
 
     short_name = project_name.split("//")[-1] if "//" in project_name else project_name
diff --git a/ftl_project_expert/prompts/summary.py b/ftl_project_expert/prompts/summary.py
index 8885135..0b33816 100644
--- a/ftl_project_expert/prompts/summary.py
+++ b/ftl_project_expert/prompts/summary.py
@@ -5,12 +5,17 @@ def build_summary_prompt(
     beliefs_text: str,
     project_name: str,
     belief_count: int,
+    total_count: int = 0,
 ) -> str:
     """Build a prompt for summarizing project state from beliefs."""
+    if total_count > belief_count:
+        count_line = f"## Beliefs analyzed: {belief_count} (top by impact out of {total_count} total)"
+    else:
+        count_line = f"## Beliefs analyzed: {belief_count}"
     return f"""You are a senior project manager synthesizing a comprehensive project summary from verified beliefs about a project.
 
 ## Project: {project_name}
-## Beliefs analyzed: {belief_count}
+{count_line}
 
 ## Beliefs
 

```

## Observation Results

You previously requested observations. Here are the results:

```json
{
  "build_summary_prompt_callers": {
    "symbol": "build_summary_prompt",
    "production_callers": [
      {
        "file": "ftl_project_expert/cli.py",
        "line": 21,
        "text": "build_summary_prompt,",
        "context_function": null,
        "context_snippet": "   18:     RESEARCH_PROMPT,\n   19:     build_explore_prompt,\n   20:     build_scan_prompt,\n>> 21:     build_summary_prompt,\n   22: )\n   23: from .sources import GitHubSource, GitLabSource, Issue, JiraSource\n   24: from .topics import ("
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2081,
        "text": "prompt = build_summary_prompt(",
        "context_function": "summary",
        "context_snippet": "   2078: \n   2079:     project_name = config.get(\"repo\", config.get(\"project\", \"unknown\"))\n   2080: \n>> 2081:     prompt = build_summary_prompt(\n   2082:         beliefs_text=beliefs_text,\n   2083:         project_name=project_name,\n   2084:         belief_count=belief_count,"
      },
      {
        "file": "ftl_project_expert/prompts/__init__.py",
        "line": 7,
        "text": "from .summary import build_summary_prompt",
        "context_function": null,
        "context_snippet": "   4: from .explore import build_explore_prompt\n   5: from .propose import PROPOSE_BELIEFS_PROJECT\n   6: from .research import RESEARCH_PROMPT\n>> 7: from .summary import build_summary_prompt\n   8: \n   9: __all__ = [\n   10:     \"build_scan_prompt\","
      },
      {
        "file": "ftl_project_expert/prompts/__init__.py",
        "line": 14,
        "text": "\"build_summary_prompt\",",
        "context_function": null,
        "context_snippet": "   11:     \"build_explore_prompt\",\n   12:     \"PROPOSE_BELIEFS_PROJECT\",\n   13:     \"RESEARCH_PROMPT\",\n>> 14:     \"build_summary_prompt\",\n   15: ]"
      },
      {
        "file": "ftl_project_expert/prompts/summary.py",
        "line": 4,
        "text": "def build_summary_prompt(",
        "context_function": null,
        "context_snippet": "   1: \"\"\"Summary prompt \u2014 synthesize project state from beliefs.\"\"\"\n   2: \n   3: \n>> 4: def build_summary_prompt(\n   5:     beliefs_text: str,\n   6:     project_name: str,\n   7:     belief_count: int,"
      }
    ],
    "test_callers": [],
    "production_count": 5,
    "test_count": 0,
    "total_count": 5
  },
  "summary_function_body": {
    "function": "summary",
    "file": "ftl_project_expert/cli.py",
    "start_line": 2017,
    "end_line": 2102,
    "source": "@cli.command()\n@click.pass_context\ndef summary(ctx):\n    \"\"\"Synthesize a project summary from beliefs.\"\"\"\n    config = _load_config()\n    if not config:\n        click.echo(\"Not initialized. Run: project-expert init <platform> <target>\")\n        sys.exit(1)\n\n    model = ctx.obj[\"model\"]\n    timeout = ctx.obj[\"timeout\"]\n\n    if not check_model_available(model):\n        click.echo(f\"Error: Model '{model}' CLI not available\", err=True)\n        sys.exit(1)\n\n    # Read beliefs from reasons or beliefs.md\n    max_beliefs = 500\n    beliefs_text = \"\"\n    belief_count = 0\n    total_count = 0\n    sorted_by_impact = False\n\n    if _has_reasons() and Path(\"reasons.db\").exists():\n        result = subprocess.run(\n            [\"reasons\", \"list\", \"--status\", \"IN\", \"--by-impact\"],\n            capture_output=True, text=True,\n        )\n        if result.returncode == 0 and result.stdout.strip():\n            lines = [l for l in result.stdout.splitlines() if l.strip()]\n            total_count = len(lines)\n            if total_count > max_beliefs:\n                lines = lines[:max_beliefs]\n            beliefs_text = \"\\n\".join(lines)\n            belief_count = len(lines)\n            sorted_by_impact = True\n    elif Path(\"beliefs.md\").exists():\n        full_text = Path(\"beliefs.md\").read_text()\n        sections = re.split(r\"(?=^### \\S+)\", full_text, flags=re.MULTILINE)\n        sections = [s for s in sections if s.strip()]\n        total_count = len(sections)\n        if total_count > max_beliefs:\n            sections = sections[:max_beliefs]\n        beliefs_text = \"\\n\".join(sections)\n        belief_count = len(sections)\n\n    if not beliefs_text or belief_count == 0:\n        click.echo(\"No beliefs found. Run the pipeline first:\")\n        click.echo(\"  project-expert scan\")\n        click.echo(\"  project-expert propose-beliefs\")\n        click.echo(\"  project-expert accept-beliefs\")\n        sys.exit(1)\n\n    if total_count > max_beliefs:\n        order = \"by impact\" if sorted_by_impact else \"by file order\"\n        click.echo(\n            f\"Summarizing top {belief_count} of {total_count} beliefs ({order}) with {model}...\",\n            err=True,\n        )\n    else:\n        click.echo(f\"Summarizing {belief_count} beliefs with {model}...\", err=True)\n\n    project_name = config.get(\"repo\", config.get(\"project\", \"unknown\"))\n\n    prompt = build_summary_prompt(\n        beliefs_text=beliefs_text,\n        project_name=project_name,\n        belief_count=belief_count,\n        total_count=total_count,\n    )\n\n    prompt_size_kb = len(prompt.encode()) / 1024\n    try:\n        result = asyncio.run(invoke(prompt, model, timeout=timeout))\n    except Exception as e:\n        click.echo(\n            f\"Error: Model {model} failed (prompt size: {prompt_size_kb:.0f} KB): {e}\",\n            err=True,\n        )\n        sys.exit(1)\n\n    short_name = project_name.split(\"//\")[-1] if \"//\" in project_name else project_name\n    safe_name = short_name.replace(\"/\", \"-\")\n    _create_entry(f\"summary-{safe_name}\", f\"Summary: {project_name}\", result)\n\n    _emit(ctx, result)"
  },
  "summary_tests": {
    "source_file": "ftl_project_expert/cli.py",
    "test_files": [],
    "test_count": 0
  },
  "invoke_exceptions": {
    "function": "invoke",
    "file": "ftl_project_expert/cli.py",
    "explicit_raises": [],
    "calls": [],
    "error": "Function 'invoke' not found"
  },
  "build_summary_prompt_body": {
    "function": "build_summary_prompt",
    "file": "ftl_project_expert/prompts/summary.py",
    "start_line": 4,
    "end_line": 38,
    "source": "def build_summary_prompt(\n    beliefs_text: str,\n    project_name: str,\n    belief_count: int,\n    total_count: int = 0,\n) -> str:\n    \"\"\"Build a prompt for summarizing project state from beliefs.\"\"\"\n    if total_count > belief_count:\n        count_line = f\"## Beliefs analyzed: {belief_count} (top by impact out of {total_count} total)\"\n    else:\n        count_line = f\"## Beliefs analyzed: {belief_count}\"\n    return f\"\"\"You are a senior project manager synthesizing a comprehensive project summary from verified beliefs about a project.\n\n## Project: {project_name}\n{count_line}\n\n## Beliefs\n\n{beliefs_text}\n\n## Instructions\n\nSynthesize these beliefs into a single, authoritative project summary. This should be the document someone reads to understand the project's current state in 5 minutes.\n\n1. **Project Overview** \u2014 What is this project? What does it do? Who uses it?\n2. **Architecture** \u2014 Key components, integrations, and technology stack\n3. **Current State** \u2014 What's actively being worked on? What's the overall health?\n4. **Key Risks** \u2014 The top 3-5 risks, ordered by impact. Be specific about why each matters.\n5. **Team & Ownership** \u2014 Who's doing what? Where are the gaps?\n6. **Milestones & Deadlines** \u2014 Any visible milestones, their status, and risk level\n7. **Patterns & Themes** \u2014 Cross-cutting concerns that appear across multiple beliefs\n8. **Recommendations** \u2014 Top 3-5 actions the team should take, ordered by priority\n\nBe concrete \u2014 reference issue IDs, team members, and specific beliefs. Avoid generic advice. If beliefs contradict each other, note the contradiction.\n\"\"\""
  }
}
```

Use these results to inform your review. Do not request the same observations again.


## Instructions

For each significant change (new file, modified function, etc.), provide a structured verdict.

Use this exact format for each change:

### <file_path or file_path:function_name>
VERDICT: PASS | CONCERN | BLOCK
CORRECTNESS: VALID | QUESTIONABLE | BROKEN
SPEC_COMPLIANCE: MEETS | PARTIAL | VIOLATES | N/A
ISSUE_COMPLIANCE: ADDRESSES | PARTIAL | UNRELATED | N/A
BELIEF_COMPLIANCE: CONSISTENT | VIOLATES | N/A
TEST_COVERAGE: COVERED | PARTIAL | UNTESTED
INTEGRATION: WIRED | PARTIAL | MISSING
REASONING: <brief explanation of your assessment>
---

## Review Criteria

1. **CORRECTNESS**: Does the code do what it claims? Is the logic sound?
   - VALID: Logic is correct, no bugs apparent
   - QUESTIONABLE: Logic may have edge cases or unclear behavior
   - BROKEN: Clear bugs or incorrect behavior

2. **SPEC_COMPLIANCE**: Does it meet MUST requirements from the spec?
   - MEETS: All relevant spec requirements satisfied
   - PARTIAL: Some requirements met, others missing or incomplete
   - VIOLATES: Contradicts spec requirements
   - N/A: No spec provided or not applicable

3. **ISSUE_COMPLIANCE** (only when an issue is provided): Do the changes address the problem or feature described in the issue?
   - ADDRESSES: Changes directly solve the issue's stated problem or implement the requested feature
   - PARTIAL: Changes partially address the issue but leave some aspects unresolved
   - UNRELATED: Changes do not appear related to the issue
   - N/A: No issue provided

4. **TEST_COVERAGE**: Are there tests for the new/changed code?
   - COVERED: Tests exist and cover the changes
   - PARTIAL: Some tests exist but coverage is incomplete
   - UNTESTED: No tests for the changes

5. **INTEGRATION**: Are callers updated? Is the feature usable end-to-end?
   - WIRED: Feature is fully integrated and usable
   - PARTIAL: Interface exists but callers not updated, or integration incomplete
   - MISSING: No integration with existing code

6. **BELIEF_COMPLIANCE** (only when beliefs are provided): Do the changes respect known architectural invariants, contracts, and rules?
   - CONSISTENT: Changes align with or reinforce known beliefs
   - VIOLATES: Changes contradict a specific belief — cite the belief ID
   - N/A: No beliefs provided or no relevant beliefs apply

## Verdict Guidelines

- **BLOCK**: Security issues, broken functionality, spec violations, or missing critical integration
- **CONCERN**: Missing tests, partial integration, questionable patterns, or unclear logic
- **PASS**: Correct, tested, well-integrated code

## Important

- Full function bodies for modified functions may be available in the observations section — use them to verify the complete logic, not just the diff hunks
- Related test files (prefixed with ``related_test:``) may be included in observations — check whether existing test assertions still match modified return types, signatures, or behavior. Flag any test that would break due to the changes
- If duplicate test coverage is detected (multiple test files covering the same source), note it in your review
- Focus on actual issues, not style preferences
- If a method signature is added but callers aren't updated, that's PARTIAL integration
- Be specific in reasoning - reference line numbers or function names
- When in doubt, use CONCERN rather than PASS

## Self-Review

After completing your review, add a brief self-assessment:

### SELF_REVIEW
LIMITATIONS: <what context were you missing that affected review quality?>
---

Examples of limitations:
- "Could not see full class to verify no other methods access the modified field"
- "Test file not included in diff - cannot verify coverage claims"
- "Spec file referenced but not provided"


## Feature Requests

If this review tool could be improved to help you do a better job, suggest features:

### FEATURE_REQUESTS
- <suggestion 1>
- <suggestion 2>
---

Examples:
- "Include full file context for modified functions, not just diff hunks"
- "Show callers of modified methods to verify integration"
- "Include test file alongside implementation changes"

Only include this section if you have specific suggestions. Skip if none.
