---
phase: 12-doc-search-integration
plan: 03
type: execute
wave: 2
depends_on:
  - 12-01
files_modified:
  - src/ztlctl/mcp/resources.py
  - tests/mcp/test_resources.py
autonomous: true
requirements:
  - AGNT-04

must_haves:
  truths:
    - "ztlctl://docs/index is in _RESOURCE_CATALOG and register_resources() wires it"
    - "ztlctl://docs/search is in _RESOURCE_CATALOG and register_resources() wires it as a static resource that returns search guidance"
    - "docs_index_resource() returns the content of docs/llms.txt"
    - "docs_search_resource() returns a JSON guidance message instructing agents to use the docs_search MCP tool for queries"
    - "Both _impl functions are testable without the mcp package"
    - "resource_catalog() includes both new URIs"
  artifacts:
    - path: "src/ztlctl/mcp/resources.py"
      provides: "_docs_index_impl wiring, _docs_search_resource_impl, two new @server.resource decorators, two new _RESOURCE_CATALOG entries"
      exports:
        - "docs_index_impl (public, callable from tests)"
        - "docs_search_resource_impl (public, callable from tests)"
    - path: "tests/mcp/test_resources.py"
      provides: "Tests for docs_index_impl, docs_search_resource_impl, and catalog presence"
  key_links:
    - from: "src/ztlctl/mcp/resources.py"
      to: "src/ztlctl/services/docs.py"
      via: "lazy import of _docs_search_impl and _docs_index_impl inside _impl functions"
      pattern: "from ztlctl.services.docs import _docs_search_impl, _docs_index_impl"
    - from: "src/ztlctl/mcp/resources.py register_resources()"
      to: "docs_index_impl, docs_search_resource_impl"
      via: "@server.resource decorator"
      pattern: "@server.resource(\"ztlctl://docs/index\")"
---

<objective>
Add two MCP resources for documentation: `ztlctl://docs/index` (static, serves llms.txt) and `ztlctl://docs/search` (static guidance resource directing agents to use the `docs_search` MCP tool).

Purpose: Agents can access the docs index directly as an MCP resource. The `ztlctl://docs/search` resource is a static guidance resource (not parameterized) because the mcp>=1.0 library in this codebase only uses static @server.resource decorators — parameterized URI templates have not been tested in this codebase. Actual search is handled by the `docs_search` MCP tool (auto-generated from the ActionDefinition registered in Plan 02). This is the reliable, proven pattern.
Output: Two new entries in _RESOURCE_CATALOG and two new resource registrations in register_resources().
</objective>

<execution_context>
@/Users/shparki/.claude/get-shit-done/workflows/execute-plan.md
@/Users/shparki/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/12-doc-search-integration/12-CONTEXT.md
@.planning/phases/12-doc-search-integration/12-RESEARCH.md
@.planning/phases/12-doc-search-integration/12-01-SUMMARY.md

<interfaces>
<!-- Exact patterns from existing codebase. Executor uses these directly. -->

From src/ztlctl/mcp/resources.py — _RESOURCE_CATALOG tuple (add two entries here):
```python
_RESOURCE_CATALOG: tuple[dict[str, str], ...] = (
    {"uri": "ztlctl://context", "description": "Full vault context: identity, methodology, and overview."},
    # ... 13 more entries ...
    {"uri": "ztlctl://agent-reference", "description": "Agent reference: onboarding flow, tool catalog, workflows, and errors."},
    # ADD AT END:
    {"uri": "ztlctl://docs/index", "description": "Navigation map of all ztlctl documentation pages."},
    {"uri": "ztlctl://docs/search", "description": "Documentation search guidance — use the docs_search tool for actual queries."},
)
```

From src/ztlctl/mcp/resources.py — static resource _impl functions (pattern to follow):
```python
def capture_spec_impl(_vault: Any) -> dict[str, Any]:
    """Return the agent-facing capture contract for source bundle ingest."""
    return { ... }  # pure, no vault dependency despite _vault arg
```

New _impl functions to add (vault-independent, but still take _vault for pattern consistency):
```python
def docs_index_impl(_vault: Any = None) -> str:
    """Return navigation map of all documentation pages (content of docs/llms.txt)."""
    from ztlctl.services.docs import _docs_index_impl
    return _docs_index_impl()

def docs_search_resource_impl(_vault: Any = None) -> dict[str, Any]:
    """Guidance for agents: use docs_search tool for documentation queries."""
    return {
        "info": (
            "To search the documentation corpus, use the 'docs_search' MCP tool. "
            "Example: docs_search(query='how to create a note', limit=5)."
        ),
        "tool": "docs_search",
        "params": {"query": "string (required)", "limit": "int (default: 5)"},
    }
```

Note on naming: existing resource impl functions are named without leading underscore when they're
public-facing (e.g., overview_impl, capture_spec_impl, agent_reference_impl). Follow the same pattern
for docs_index_impl and docs_search_resource_impl. Tests import these directly.

From src/ztlctl/mcp/resources.py — register_resources() pattern:
```python
def register_resources(server: Any, vault: Any) -> None:
    """Register core and plugin MCP resources on the FastMCP server."""

    @server.resource("ztlctl://context")  # type: ignore[untyped-decorator]
    def context_resource() -> str:
        """Full vault context: identity, methodology, and overview."""
        import json
        return json.dumps(context_impl(vault), indent=2)
    # ...
```

Add at end of register_resources() (before the plugin_manager section):
```python
    @server.resource("ztlctl://docs/index")  # type: ignore[untyped-decorator]
    def docs_index_resource() -> str:
        """Navigation map of all ztlctl documentation pages."""
        return docs_index_impl(vault)

    @server.resource("ztlctl://docs/search")  # type: ignore[untyped-decorator]
    def docs_search_guide_resource() -> str:
        """Documentation search guidance — use the docs_search tool for actual queries."""
        import json
        return json.dumps(docs_search_resource_impl(vault), indent=2)
```

From tests/mcp/test_resources.py — existing test pattern to extend:
```python
# Existing test pattern — check how _impl functions are tested
# Find the TestResourceCatalog class or similar and add two new assertions
```

Read tests/mcp/test_resources.py before writing new tests to match the existing fixture
and class structure. Do NOT replace existing tests — only ADD new test cases.
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add docs_index_impl and docs_search_resource_impl to resources.py</name>
  <files>
    src/ztlctl/mcp/resources.py,
    tests/mcp/test_resources.py
  </files>
  <read_first>
    - src/ztlctl/mcp/resources.py lines 1-75 (_RESOURCE_CATALOG and resource_catalog())
    - src/ztlctl/mcp/resources.py lines 615-741 (register_resources() full function)
    - tests/mcp/test_resources.py (full file — understand existing test structure before adding)
    - src/ztlctl/services/docs.py (_docs_index_impl and _docs_search_impl signatures from Plan 01)
  </read_first>
  <behavior>
    - _RESOURCE_CATALOG contains entry with uri "ztlctl://docs/index"
    - _RESOURCE_CATALOG contains entry with uri "ztlctl://docs/search"
    - resource_catalog(vault=None) returns tuple including both new URIs
    - docs_index_impl() returns a non-empty string (content of docs/llms.txt or error string)
    - docs_search_resource_impl() returns a dict with "tool" key equal to "docs_search"
    - docs_index_impl is importable from ztlctl.mcp.resources
    - docs_search_resource_impl is importable from ztlctl.mcp.resources
  </behavior>
  <action>
    Write tests first (RED), then implement (GREEN).

    1. Add to tests/mcp/test_resources.py (do NOT replace existing tests):
       - Test that "ztlctl://docs/index" is in [e["uri"] for e in resource_catalog()]
       - Test that "ztlctl://docs/search" is in [e["uri"] for e in resource_catalog()]
       - Test docs_index_impl() returns a str (may be error string if docs/ not found in CI — that is OK)
       - Test docs_search_resource_impl() returns dict with key "tool" == "docs_search"

    2. Add to src/ztlctl/mcp/resources.py:

       a. In _RESOURCE_CATALOG tuple — append two entries at the end (before closing parenthesis):
          ```python
          {
              "uri": "ztlctl://docs/index",
              "description": "Navigation map of all ztlctl documentation pages.",
          },
          {
              "uri": "ztlctl://docs/search",
              "description": (
                  "Documentation search guidance. "
                  "Use the docs_search tool to search the corpus by query string."
              ),
          },
          ```

       b. Add two _impl functions after the last existing _impl function
          (after recipe_knowledge_synthesis_impl, before register_resources):
          ```python
          def docs_index_impl(_vault: Any = None) -> str:
              """Return navigation map of all documentation pages (content of docs/llms.txt)."""
              from ztlctl.services.docs import _docs_index_impl
              return _docs_index_impl()

          def docs_search_resource_impl(_vault: Any = None) -> dict[str, Any]:
              """Return documentation search guidance for agents."""
              return {
                  "info": (
                      "To search the documentation corpus, use the 'docs_search' MCP tool. "
                      "Call: docs_search(query='your question', limit=5)."
                  ),
                  "tool": "docs_search",
                  "params": {
                      "query": "string (required) — search terms",
                      "limit": "int (default: 5) — maximum results",
                  },
              }
          ```

       c. In register_resources() — add two new resource registrations immediately before
          the `plugin_manager = getattr(vault, "plugin_manager", None)` line:
          ```python
          @server.resource("ztlctl://docs/index")  # type: ignore[untyped-decorator]
          def docs_index_resource() -> str:
              """Navigation map of all ztlctl documentation pages."""
              return docs_index_impl(vault)

          @server.resource("ztlctl://docs/search")  # type: ignore[untyped-decorator]
          def docs_search_guide_resource() -> str:
              """Documentation search guidance — use docs_search tool for queries."""
              import json
              return json.dumps(docs_search_resource_impl(vault), indent=2)
          ```
  </action>
  <verify>
    <automated>uv run pytest tests/mcp/test_resources.py -x -q</automated>
  </verify>
  <done>
    tests/mcp/test_resources.py passes (all existing tests + new docs tests).
    `uv run python -c "from ztlctl.mcp.resources import docs_index_impl, docs_search_resource_impl; print('OK')"` exits 0.
    `uv run mypy src/ztlctl/mcp/resources.py` exits 0.
    `uv run pytest` full suite passes.
  </done>
</task>

</tasks>

<verification>
- `uv run pytest tests/mcp/test_resources.py -q` passes (existing + new tests)
- `uv run pytest tests/services/test_docs.py tests/mcp/test_resources.py -q` passes
- `uv run pytest` full suite passes
- `uv run mypy src/ztlctl/mcp/resources.py` exits 0
- `uv run ruff check src/ztlctl/mcp/resources.py` exits 0
- `uv run python -c "from ztlctl.mcp.resources import resource_catalog; uris = [e['uri'] for e in resource_catalog()]; assert 'ztlctl://docs/index' in uris; assert 'ztlctl://docs/search' in uris; print('catalog OK')"` prints "catalog OK"
</verification>

<success_criteria>
`ztlctl://docs/index` and `ztlctl://docs/search` appear in resource_catalog(). docs_index_impl() returns llms.txt content. docs_search_resource_impl() returns guidance dict with "tool": "docs_search". Both registered in register_resources(). No mypy or ruff errors. Full test suite green.
</success_criteria>

<output>
After completion, create `.planning/phases/12-doc-search-integration/12-03-SUMMARY.md`
</output>
