I'll review each significant change in the diff.

### expert_build/cli.py
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: CLI arguments are correctly added following the existing pattern. `_lazy("pipeline", "cmd_pipeline")` matches the module/function naming. All argument names use underscores consistent with `argparse` conventions and match what `pipeline.py` expects. The `--pdf` uses `action="append"` correctly for repeatable args. `--depth` defaults to 2 here (vs 1 for standalone `fetch-docs`), which is a deliberate choice for the pipeline context.
---

### expert_build/pipeline.py (overall structure)
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: PARTIAL
INTEGRATION: WIRED
REASONING: Clean decomposition into stage functions with a convergence loop. `REASONS_DB` is imported from `propose` rather than redefined, avoiding drift. All downstream command functions are lazy-imported inside stage functions, consistent with `cli.py`'s `_lazy()` pattern.
---

### expert_build/pipeline.py:_stage_ingest
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: Correctly handles four combinations: fetch+pdf, fetch-only, pdf-only, no-fetch+pdf. When `no_fetch=True`, fetch is skipped regardless of whether `url` is set, and PDFs are still chunked — this is the right behavior. The `SimpleNamespace` for `fetch_args` includes all fields that `cmd_fetch_docs` reads from its args.
---

### expert_build/pipeline.py:_stage_extract
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: The `auto_accept_proposals` call rewrites markers, then `cmd_accept_beliefs` imports them. However, `cmd_accept_beliefs` at `propose.py:431` parses with `r"### \[?ACCEPT\]? (\S+)\n(.+?)\n- Source: (.+?)\n"` — the `.+?` for claim text only matches a single line. If `cmd_propose_beliefs` ever produces multi-line belief text, accepted beliefs would silently be dropped. This is a pre-existing issue in `cmd_accept_beliefs`, not introduced here, but the pipeline's auto-accept makes it more likely to surface since there's no human editing pass. Also, calling both `auto_accept_proposals` and then `cmd_accept_beliefs` is correct but the ordering matters — the function correctly rewrites markers before import. The `setattr(prop_args, "all", False)` on line 74 works but is unusual; `SimpleNamespace` accepts `all=False` in the constructor.
---

### expert_build/pipeline.py:_stage_derive
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: Correctly implements derive-until-saturated. The `isinstance(r, dict)` check on line 138 to count added beliefs works because `apply_proposals` returns `(proposal, result_dict)` tuples where `result_dict` is a dict on success. Exception handling around `invoke_sync` breaks the loop rather than crashing the pipeline. Saturation detection (empty proposals or no valid proposals) is sound.
---

### expert_build/pipeline.py:_stage_repair
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: The `research()` function at `api.py:2511`, when called with `belief_ids`, re-reviews those beliefs with `dry_run=True` before researching invalids. This means every invalid belief gets reviewed twice per cycle — once in `_stage_review` and once inside `research()`. The second review might produce different results than the first, leading to a mismatch between what the pipeline thinks is invalid (from `_stage_review`) and what `research()` actually processes. Not a bug per se — `research()` is designed this way — but it's redundant work and could confuse debugging.
---

### expert_build/pipeline.py:cmd_pipeline
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: The convergence criterion `invalid_count == 0 and added == 0` is sound — if no new beliefs are derived and none are invalid, further cycles are pointless. The `total_stages = 8` constant matches the banner numbering. Skipping repair when `invalid_count == 0` avoids unnecessary work. `_caffeinate()` prevents sleep during long runs. The export stage runs unconditionally after the loop, which is correct since it should reflect the final state.
---

### expert_build/pipeline.py:_stage_export
VERDICT: CONCERN
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Writes `network.json` and `README.md` unconditionally, which could overwrite an existing hand-edited README. This is acceptable for a pipeline context but worth noting. No unit test for this stage — it's only mocked in full pipeline tests, so its internal logic (correct export_card args, correct JSON writing) isn't verified.
---

### expert_build/pipeline.py:_stage_summarize
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Thin wrapper around `cmd_summarize`. No unit test, but the function is trivial — it constructs a `SimpleNamespace` and delegates. The `limit=None` correctly passes through, letting `cmd_summarize` process all files.
---

### expert_build/propose.py:auto_accept_proposals
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: Simple `re.sub` replacing `[ACCEPT/REJECT]` with `[ACCEPT]`. The regex is a literal string match, not a character class — `r'\[ACCEPT/REJECT\]'` matches exactly `[ACCEPT/REJECT]`, which is correct. Won't accidentally touch `[ACCEPT]` or `[REJECT]` on their own. Tests cover the three relevant cases.
---

### tests/test_pipeline.py
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: Good coverage of individual stages and full pipeline flows. The `work_dir` fixture correctly creates isolated temp dirs with necessary structure. Patch targets are correct — lazy imports inside stage functions mean patching the source module (`expert_build.fetch.cmd_fetch_docs`) works because the import happens within the patch context. The `make_pipeline_args` helper covers all CLI args. Key behaviors tested: early convergence, all-rounds execution, no-auto-accept early stop, model availability check. Missing: no tests for `_stage_export` or `_stage_summarize` internals, and no test for the `has_sources` branch when url/pdf are provided in the full pipeline.
---

### uv.lock
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: N/A
INTEGRATION: WIRED
REASONING: Version bump of `ftl-reasons` from 0.34.0 to 0.43.0 with new optional dependency groups (`cluster`, `mcp`, `api`, `vertex`, `langfuse`, `llm`). The pipeline uses `review_beliefs`, `research`, `deduplicate`, and `export_card` from `reasons_lib.api`, which are available in this version. The new `cluster` extra provides `deduplicate(semantic=True)` support, though the pipeline uses the default `auto=True` mode.
---

### SELF_REVIEW
LIMITATIONS: Could not verify that `reasons_lib.derive.apply_proposals` actually returns `(proposal, dict)` tuples on success (relied on the `isinstance(r, dict)` check in the code and the test mock). Could not run the test suite to verify all patches resolve correctly at runtime. Did not have access to the full `reasons_lib.repair.research_beliefs` implementation to confirm the `research()` re-review behavior is intentional. Could not verify whether `cmd_summarize` depends on the `entry` CLI being installed (it does, per the observation) — the pipeline would fail at the summarize stage if `entry` is not on PATH, but there's no pre-check for this.
---

### FEATURE_REQUESTS
- Show the full signature and return type of functions called by the changed code (e.g., `apply_proposals` return type) to verify type assumptions
- Flag when a pipeline/orchestration function depends on external CLI tools (like `entry`) that aren't checked at startup
---
