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

## Specification

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





## Code Changes

```diff
diff --git a/expert_build/cli.py b/expert_build/cli.py
index 00ea9ad..0c49299 100644
--- a/expert_build/cli.py
+++ b/expert_build/cli.py
@@ -138,6 +138,20 @@ def main():
         "status": lambda a: _lazy("init_cmd", "cmd_status")(a),
         "install-skill": lambda a: _lazy("init_cmd", "cmd_install_skill")(a),
     }
+
+    subparser_names = set(sub.choices.keys())
+    command_names = set(commands.keys())
+    if subparser_names != command_names:
+        missing_dispatch = subparser_names - command_names
+        missing_parser = command_names - subparser_names
+        parts = []
+        if missing_dispatch:
+            parts.append(f"subcommands without dispatch: {missing_dispatch}")
+        if missing_parser:
+            parts.append(f"dispatch keys without subcommand: {missing_parser}")
+        print(f"CLI registration error: {'; '.join(parts)}", file=sys.stderr)
+        sys.exit(1)
+
     commands[args.command](args)
 
 

```

## Observation Results

You previously requested observations. Here are the results:

```json
{
  "main_function_body": {
    "function": "main",
    "file": "expert_build/cli.py",
    "start_line": 17,
    "end_line": 155,
    "source": "def main():\n    parser = argparse.ArgumentParser(\n        prog=\"expert-build\",\n        description=\"Build expert agents from documented domains\",\n    )\n    parser.add_argument(\"--version\", action=\"version\", version=f\"%(prog)s {__version__}\")\n\n    sub = parser.add_subparsers(dest=\"command\")\n\n    # -- init --\n    init_p = sub.add_parser(\"init\", help=\"Bootstrap a new expert agent repo\")\n    init_p.add_argument(\"name\", help=\"Domain name (e.g., rhcsa, kubernetes)\")\n    init_p.add_argument(\"--domain\", help=\"One-line domain description\")\n    init_p.add_argument(\"--no-git\", action=\"store_true\", help=\"Skip git init (for subdirectories of existing repos)\")\n\n    # -- fetch-docs --\n    fetch_p = sub.add_parser(\"fetch-docs\", help=\"Fetch documentation from URLs\")\n    fetch_p.add_argument(\"url\", help=\"Starting URL to fetch\")\n    fetch_p.add_argument(\"--depth\", type=int, default=1, help=\"Crawl depth (default: 1)\")\n    fetch_p.add_argument(\"--output-dir\", default=\"sources\", help=\"Output directory (default: sources)\")\n    fetch_p.add_argument(\"--selector\", default=\"main,article,.content,body\",\n                         help=\"CSS selectors for content (comma-separated, default: main,article,.content,body)\")\n    fetch_p.add_argument(\"--sitemap\", action=\"store_true\", help=\"Use sitemap.xml for URL discovery\")\n    fetch_p.add_argument(\"--include\", help=\"URL pattern to include (glob)\")\n    fetch_p.add_argument(\"--exclude\", help=\"URL pattern to exclude (glob)\")\n    fetch_p.add_argument(\"--delay\", type=float, default=1.0, help=\"Delay between requests in seconds (default: 1.0)\")\n\n    # -- chunk-pdf --\n    chunk_p = sub.add_parser(\"chunk-pdf\", help=\"Chunk a PDF paper into section entries\")\n    chunk_p.add_argument(\"pdf\", help=\"Path to PDF file\")\n    chunk_p.add_argument(\"--prefix\", help=\"Entry filename prefix (e.g., 'doyle-1979')\")\n    chunk_p.add_argument(\"--source-label\", help=\"Citation label for Source line\")\n    chunk_p.add_argument(\"--dry-run\", action=\"store_true\", help=\"Show sections without creating entries\")\n\n    # -- summarize --\n    sum_p = sub.add_parser(\"summarize\", help=\"Generate entries from source documents\")\n    sum_p.add_argument(\"--input-dir\", default=\"sources\", help=\"Source directory (default: sources)\")\n    sum_p.add_argument(\"--limit\", type=int, help=\"Max files to process\")\n    sum_p.add_argument(\"--model\", default=\"claude\", help=\"Model to use (default: claude)\")\n\n    # -- propose-beliefs --\n    prop_p = sub.add_parser(\"propose-beliefs\", help=\"Extract candidate beliefs from entries\")\n    prop_p.add_argument(\"--input-dir\", default=\"entries\", help=\"Entries directory (default: entries)\")\n    prop_p.add_argument(\"--output\", default=\"proposed-beliefs.md\",\n                        help=\"Output file (default: proposed-beliefs.md)\")\n    prop_p.add_argument(\"--model\", default=\"claude\", help=\"Model to use (default: claude)\")\n    prop_p.add_argument(\"--batch-size\", type=int, default=5,\n                        help=\"Entries per LLM batch (default: 5)\")\n    prop_p.add_argument(\"--entry\", action=\"append\",\n                        help=\"Process specific entry file(s) instead of all entries\")\n    prop_p.add_argument(\"--all\", action=\"store_true\",\n                        help=\"Re-process all entries (ignore processed tracking)\")\n\n    # -- accept-beliefs --\n    accept_p = sub.add_parser(\"accept-beliefs\", help=\"Import accepted beliefs from proposals\")\n    accept_p.add_argument(\"--file\", default=\"proposed-beliefs.md\",\n                          help=\"Proposals file (default: proposed-beliefs.md)\")\n\n    # -- cert-coverage --\n    cert_p = sub.add_parser(\"cert-coverage\", help=\"Map cert objectives to beliefs\")\n    cert_p.add_argument(\"objectives_file\", help=\"Path to certification objectives\")\n    cert_p.add_argument(\"--beliefs-file\", type=Path, default=Path(\"reasons.db\"))\n    cert_p.add_argument(\"--model\", default=None, help=\"Use LLM for semantic matching\")\n\n    # -- exam --\n    exam_p = sub.add_parser(\"exam\", help=\"Run practice questions, discover gaps\")\n    exam_p.add_argument(\"questions_file\", help=\"Path to practice questions\")\n    exam_p.add_argument(\"--model\", default=\"claude\", help=\"Model to use (default: claude)\")\n    exam_p.add_argument(\"--beliefs-file\", type=Path, default=Path(\"reasons.db\"))\n    exam_p.add_argument(\"--limit\", type=int, help=\"Max questions to process\")\n    exam_p.add_argument(\"--output\", \"-o\", type=Path, default=None,\n                        help=\"Save results to file (markdown)\")\n    exam_p.add_argument(\"--no-judge\", action=\"store_true\",\n                        help=\"Disable LLM judge for open-ended questions (use string matching)\")\n\n    # -- pipeline --\n    pipe_p = sub.add_parser(\"pipeline\", help=\"Run end-to-end EEM construction pipeline\")\n    pipe_p.add_argument(\"--url\", help=\"Starting URL for doc fetching\")\n    pipe_p.add_argument(\"--pdf\", action=\"append\", help=\"PDF files to chunk (repeatable)\")\n    pipe_p.add_argument(\"--sources-dir\", default=\"sources\", help=\"Source directory (default: sources)\")\n    pipe_p.add_argument(\"--model\", default=\"claude\", help=\"Model for LLM calls (default: claude)\")\n    pipe_p.add_argument(\"--rounds\", type=int, default=3,\n                        help=\"Max derive/review/repair cycles (default: 3)\")\n    pipe_p.add_argument(\"--max-derive-rounds\", type=int, default=10,\n                        help=\"Max derive exhaust rounds per cycle (default: 10)\")\n    pipe_p.add_argument(\"--no-auto-accept\", action=\"store_true\",\n                        help=\"Stop after propose-beliefs for human review\")\n    pipe_p.add_argument(\"--no-fetch\", action=\"store_true\",\n                        help=\"Skip fetch-docs (use existing sources/)\")\n    pipe_p.add_argument(\"--depth\", type=int, default=2,\n                        help=\"Crawl depth for fetch-docs (default: 2)\")\n    pipe_p.add_argument(\"--timeout\", type=int, default=600,\n                        help=\"LLM timeout in seconds (default: 600)\")\n    pipe_p.add_argument(\"--domain\", help=\"Domain description for derive context\")\n    pipe_p.add_argument(\"--resume\", action=\"store_true\",\n                        help=\"Resume from last saved pipeline state\")\n\n    # -- status --\n    sub.add_parser(\"status\", help=\"Show pipeline progress\")\n\n    # -- install-skill --\n    skill_p = sub.add_parser(\"install-skill\", help=\"Install Claude Code skill file\")\n    skill_p.add_argument(\"--skill-dir\", type=Path, default=Path(\".claude/skills\"),\n                         help=\"Target skills directory\")\n\n    args = parser.parse_args()\n\n    if not args.command:\n        parser.print_help()\n        sys.exit(1)\n\n    commands = {\n        \"init\": lambda a: _lazy(\"init_cmd\", \"cmd_init\")(a),\n        \"chunk-pdf\": lambda a: _lazy(\"chunk_pdf\", \"cmd_chunk_pdf\")(a),\n        \"fetch-docs\": lambda a: _lazy(\"fetch\", \"cmd_fetch_docs\")(a),\n        \"summarize\": lambda a: _lazy(\"summarize\", \"cmd_summarize\")(a),\n        \"propose-beliefs\": lambda a: _lazy(\"propose\", \"cmd_propose_beliefs\")(a),\n        \"accept-beliefs\": lambda a: _lazy(\"propose\", \"cmd_accept_beliefs\")(a),\n        \"cert-coverage\": lambda a: _lazy(\"coverage\", \"cmd_cert_coverage\")(a),\n        \"exam\": lambda a: _lazy(\"exam\", \"cmd_exam\")(a),\n        \"pipeline\": lambda a: _lazy(\"pipeline\", \"cmd_pipeline\")(a),\n        \"status\": lambda a: _lazy(\"init_cmd\", \"cmd_status\")(a),\n        \"install-skill\": lambda a: _lazy(\"init_cmd\", \"cmd_install_skill\")(a),\n    }\n\n    subparser_names = set(sub.choices.keys())\n    command_names = set(commands.keys())\n    if subparser_names != command_names:\n        missing_dispatch = subparser_names - command_names\n        missing_parser = command_names - subparser_names\n        parts = []\n        if missing_dispatch:\n            parts.append(f\"subcommands without dispatch: {missing_dispatch}\")\n        if missing_parser:\n            parts.append(f\"dispatch keys without subcommand: {missing_parser}\")\n        print(f\"CLI registration error: {'; '.join(parts)}\", file=sys.stderr)\n        sys.exit(1)\n\n    commands[args.command](args)"
  },
  "main_callers": {
    "symbol": "main",
    "production_callers": [
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_async/http_proxy.py",
        "line": 95,
        "text": "that will be maintained in the pool.",
        "context_function": "__init__",
        "context_snippet": "   92:                 the pool should allow. Any attempt to send a request on a pool that\n   93:                 would exceed this amount will block until a connection is available.\n   94:             max_keepalive_connections: The maximum number of idle HTTP connections\n>> 95:                 that will be maintained in the pool.\n   96:             keepalive_expiry: The duration in seconds that an idle HTTP connection\n   97:                 may be maintained for before being expired from the pool.\n   98:             http1: A boolean indicating if HTTP/1.1 requests should be supported"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_async/http_proxy.py",
        "line": 97,
        "text": "may be maintained for before being expired from the pool.",
        "context_function": "__init__",
        "context_snippet": "   94:             max_keepalive_connections: The maximum number of idle HTTP connections\n   95:                 that will be maintained in the pool.\n   96:             keepalive_expiry: The duration in seconds that an idle HTTP connection\n>> 97:                 may be maintained for before being expired from the pool.\n   98:             http1: A boolean indicating if HTTP/1.1 requests should be supported\n   99:                 by the connection pool. Defaults to True.\n   100:             http2: A boolean indicating if HTTP/2 requests should be supported by"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_async/http_proxy.py",
        "line": 109,
        "text": "uds: Path to a Unix Domain Socket to use instead of TCP sockets.",
        "context_function": "__init__",
        "context_snippet": "   106:                 `local_address=\"0.0.0.0\"` will connect using an `AF_INET` address\n   107:                 (IPv4), while using `local_address=\"::\"` will connect using an\n   108:                 `AF_INET6` address (IPv6).\n>> 109:             uds: Path to a Unix Domain Socket to use instead of TCP sockets.\n   110:             network_backend: A backend instance to use for handling network I/O.\n   111:         \"\"\"\n   112:         super().__init__("
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_async/socks_proxy.py",
        "line": 136,
        "text": "that will be maintained in the pool.",
        "context_function": "__init__",
        "context_snippet": "   133:                 the pool should allow. Any attempt to send a request on a pool that\n   134:                 would exceed this amount will block until a connection is available.\n   135:             max_keepalive_connections: The maximum number of idle HTTP connections\n>> 136:                 that will be maintained in the pool.\n   137:             keepalive_expiry: The duration in seconds that an idle HTTP connection\n   138:                 may be maintained for before being expired from the pool.\n   139:             http1: A boolean indicating if HTTP/1.1 requests should be supported"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_async/socks_proxy.py",
        "line": 138,
        "text": "may be maintained for before being expired from the pool.",
        "context_function": "__init__",
        "context_snippet": "   135:             max_keepalive_connections: The maximum number of idle HTTP connections\n   136:                 that will be maintained in the pool.\n   137:             keepalive_expiry: The duration in seconds that an idle HTTP connection\n>> 138:                 may be maintained for before being expired from the pool.\n   139:             http1: A boolean indicating if HTTP/1.1 requests should be supported\n   140:                 by the connection pool. Defaults to True.\n   141:             http2: A boolean indicating if HTTP/2 requests should be supported by"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_async/socks_proxy.py",
        "line": 150,
        "text": "uds: Path to a Unix Domain Socket to use instead of TCP sockets.",
        "context_function": "__init__",
        "context_snippet": "   147:                 `local_address=\"0.0.0.0\"` will connect using an `AF_INET` address\n   148:                 (IPv4), while using `local_address=\"::\"` will connect using an\n   149:                 `AF_INET6` address (IPv6).\n>> 150:             uds: Path to a Unix Domain Socket to use instead of TCP sockets.\n   151:             network_backend: A backend instance to use for handling network I/O.\n   152:         \"\"\"\n   153:         super().__init__("
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_async/connection_pool.py",
        "line": 74,
        "text": "that will be maintained in the pool.",
        "context_function": "__init__",
        "context_snippet": "   71:                 the pool should allow. Any attempt to send a request on a pool that\n   72:                 would exceed this amount will block until a connection is available.\n   73:             max_keepalive_connections: The maximum number of idle HTTP connections\n>> 74:                 that will be maintained in the pool.\n   75:             keepalive_expiry: The duration in seconds that an idle HTTP connection\n   76:                 may be maintained for before being expired from the pool.\n   77:             http1: A boolean indicating if HTTP/1.1 requests should be supported"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_async/connection_pool.py",
        "line": 76,
        "text": "may be maintained for before being expired from the pool.",
        "context_function": "__init__",
        "context_snippet": "   73:             max_keepalive_connections: The maximum number of idle HTTP connections\n   74:                 that will be maintained in the pool.\n   75:             keepalive_expiry: The duration in seconds that an idle HTTP connection\n>> 76:                 may be maintained for before being expired from the pool.\n   77:             http1: A boolean indicating if HTTP/1.1 requests should be supported\n   78:                 by the connection pool. Defaults to True.\n   79:             http2: A boolean indicating if HTTP/2 requests should be supported by"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_async/connection_pool.py",
        "line": 87,
        "text": "uds: Path to a Unix Domain Socket to use instead of TCP sockets.",
        "context_function": "__init__",
        "context_snippet": "   84:                 using a particular address family. Using `local_address=\"0.0.0.0\"`\n   85:                 will connect using an `AF_INET` address (IPv4), while using\n   86:                 `local_address=\"::\"` will connect using an `AF_INET6` address (IPv6).\n>> 87:             uds: Path to a Unix Domain Socket to use instead of TCP sockets.\n   88:             network_backend: A backend instance to use for handling network I/O.\n   89:             socket_options: Socket options that have to be included\n   90:              in the TCP socket when the connection was established."
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_sync/http_proxy.py",
        "line": 95,
        "text": "that will be maintained in the pool.",
        "context_function": "__init__",
        "context_snippet": "   92:                 the pool should allow. Any attempt to send a request on a pool that\n   93:                 would exceed this amount will block until a connection is available.\n   94:             max_keepalive_connections: The maximum number of idle HTTP connections\n>> 95:                 that will be maintained in the pool.\n   96:             keepalive_expiry: The duration in seconds that an idle HTTP connection\n   97:                 may be maintained for before being expired from the pool.\n   98:             http1: A boolean indicating if HTTP/1.1 requests should be supported"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_sync/http_proxy.py",
        "line": 97,
        "text": "may be maintained for before being expired from the pool.",
        "context_function": "__init__",
        "context_snippet": "   94:             max_keepalive_connections: The maximum number of idle HTTP connections\n   95:                 that will be maintained in the pool.\n   96:             keepalive_expiry: The duration in seconds that an idle HTTP connection\n>> 97:                 may be maintained for before being expired from the pool.\n   98:             http1: A boolean indicating if HTTP/1.1 requests should be supported\n   99:                 by the connection pool. Defaults to True.\n   100:             http2: A boolean indicating if HTTP/2 requests should be supported by"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_sync/http_proxy.py",
        "line": 109,
        "text": "uds: Path to a Unix Domain Socket to use instead of TCP sockets.",
        "context_function": "__init__",
        "context_snippet": "   106:                 `local_address=\"0.0.0.0\"` will connect using an `AF_INET` address\n   107:                 (IPv4), while using `local_address=\"::\"` will connect using an\n   108:                 `AF_INET6` address (IPv6).\n>> 109:             uds: Path to a Unix Domain Socket to use instead of TCP sockets.\n   110:             network_backend: A backend instance to use for handling network I/O.\n   111:         \"\"\"\n   112:         super().__init__("
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_sync/socks_proxy.py",
        "line": 136,
        "text": "that will be maintained in the pool.",
        "context_function": "__init__",
        "context_snippet": "   133:                 the pool should allow. Any attempt to send a request on a pool that\n   134:                 would exceed this amount will block until a connection is available.\n   135:             max_keepalive_connections: The maximum number of idle HTTP connections\n>> 136:                 that will be maintained in the pool.\n   137:             keepalive_expiry: The duration in seconds that an idle HTTP connection\n   138:                 may be maintained for before being expired from the pool.\n   139:             http1: A boolean indicating if HTTP/1.1 requests should be supported"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_sync/socks_proxy.py",
        "line": 138,
        "text": "may be maintained for before being expired from the pool.",
        "context_function": "__init__",
        "context_snippet": "   135:             max_keepalive_connections: The maximum number of idle HTTP connections\n   136:                 that will be maintained in the pool.\n   137:             keepalive_expiry: The duration in seconds that an idle HTTP connection\n>> 138:                 may be maintained for before being expired from the pool.\n   139:             http1: A boolean indicating if HTTP/1.1 requests should be supported\n   140:                 by the connection pool. Defaults to True.\n   141:             http2: A boolean indicating if HTTP/2 requests should be supported by"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_sync/socks_proxy.py",
        "line": 150,
        "text": "uds: Path to a Unix Domain Socket to use instead of TCP sockets.",
        "context_function": "__init__",
        "context_snippet": "   147:                 `local_address=\"0.0.0.0\"` will connect using an `AF_INET` address\n   148:                 (IPv4), while using `local_address=\"::\"` will connect using an\n   149:                 `AF_INET6` address (IPv6).\n>> 150:             uds: Path to a Unix Domain Socket to use instead of TCP sockets.\n   151:             network_backend: A backend instance to use for handling network I/O.\n   152:         \"\"\"\n   153:         super().__init__("
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_sync/connection_pool.py",
        "line": 74,
        "text": "that will be maintained in the pool.",
        "context_function": "__init__",
        "context_snippet": "   71:                 the pool should allow. Any attempt to send a request on a pool that\n   72:                 would exceed this amount will block until a connection is available.\n   73:             max_keepalive_connections: The maximum number of idle HTTP connections\n>> 74:                 that will be maintained in the pool.\n   75:             keepalive_expiry: The duration in seconds that an idle HTTP connection\n   76:                 may be maintained for before being expired from the pool.\n   77:             http1: A boolean indicating if HTTP/1.1 requests should be supported"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_sync/connection_pool.py",
        "line": 76,
        "text": "may be maintained for before being expired from the pool.",
        "context_function": "__init__",
        "context_snippet": "   73:             max_keepalive_connections: The maximum number of idle HTTP connections\n   74:                 that will be maintained in the pool.\n   75:             keepalive_expiry: The duration in seconds that an idle HTTP connection\n>> 76:                 may be maintained for before being expired from the pool.\n   77:             http1: A boolean indicating if HTTP/1.1 requests should be supported\n   78:                 by the connection pool. Defaults to True.\n   79:             http2: A boolean indicating if HTTP/2 requests should be supported by"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/httpcore/_sync/connection_pool.py",
        "line": 87,
        "text": "uds: Path to a Unix Domain Socket to use instead of TCP sockets.",
        "context_function": "__init__",
        "context_snippet": "   84:                 using a particular address family. Using `local_address=\"0.0.0.0\"`\n   85:                 will connect using an `AF_INET` address (IPv4), while using\n   86:                 `local_address=\"::\"` will connect using an `AF_INET6` address (IPv6).\n>> 87:             uds: Path to a Unix Domain Socket to use instead of TCP sockets.\n   88:             network_backend: A backend instance to use for handling network I/O.\n   89:             socket_options: Socket options that have to be included\n   90:              in the TCP socket when the connection was established."
      },
      {
        "file": ".venv/lib/python3.14/site-packages/h11/_readers.py",
        "line": 125,
        "text": "self._remaining = length",
        "context_function": "__init__",
        "context_snippet": "   122: class ContentLengthReader:\n   123:     def __init__(self, length: int) -> None:\n   124:         self._length = length\n>> 125:         self._remaining = length\n   126: \n   127:     def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:\n   128:         if self._remaining == 0:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/h11/_readers.py",
        "line": 128,
        "text": "if self._remaining == 0:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/h11/_readers.py",
        "line": 130,
        "text": "data = buf.maybe_extract_at_most(self._remaining)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/h11/_readers.py",
        "line": 133,
        "text": "self._remaining -= len(data)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/h11/_readers.py",
        "line": 140,
        "text": "self._length - self._remaining, self._length"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/h11/_connection.py",
        "line": 1,
        "text": "# This contains the main Connection class. Everything in h11 revolves around"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/h11/_connection.py",
        "line": 146,
        "text": "# The main Connection class"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/soupsieve/pretty.py",
        "line": 7,
        "text": "It is mainly geared towards our types as the `SelectorList`"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/idna/core.py",
        "line": 332,
        "text": "def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False) -> str:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/idna/core.py",
        "line": 338,
        "text": "for pos, char in enumerate(domain):"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/idna/core.py",
        "line": 360,
        "text": "\"Codepoint {} not allowed at position {} in {}\".format(_unot(code_point), pos + 1, repr(domain))"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/idna/core.py",
        "line": 387,
        "text": "raise IDNAError(\"Empty domain\")"
      }
    ],
    "test_callers": [
      {
        "file": "tests/test_pipeline.py",
        "line": 45,
        "text": "domain=\"Test domain\",",
        "context_function": "make_pipeline_args",
        "context_snippet": "   42:         no_fetch=False,\n   43:         depth=2,\n   44:         timeout=600,\n>> 45:         domain=\"Test domain\",\n   46:         resume=False,\n   47:     )\n   48:     defaults.update(overrides)"
      }
    ],
    "production_count": 206,
    "test_count": 1,
    "total_count": 207
  },
  "main_tests": {
    "source_file": "expert_build/cli.py",
    "test_files": [],
    "test_count": 0
  },
  "file_imports": {
    "file": "expert_build/cli.py",
    "imports": [
      "argparse",
      "importlib",
      "sys"
    ],
    "from_imports": [
      {
        "module": "pathlib",
        "names": [
          "Path"
        ]
      },
      {
        "module": "",
        "names": [
          "__version__"
        ]
      }
    ],
    "import_section": "\"\"\"Expert agent builder CLI.\"\"\"\n\nimport argparse\nimport importlib\nimport sys\nfrom pathlib import Path\n\nfrom . import __version__\n\n\ndef _lazy(module_name, func_name):\n    \"\"\"Lazy import to keep startup fast.\"\"\""
  }
}
```

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.
