---
phase: 19-methodology-guidance-and-polaris
plan: 03
type: execute
wave: 2
depends_on: [19-01]
files_modified:
  - src/ztlctl/actions/_check.py
  - src/ztlctl/controllers/check.py
  - src/ztlctl/services/check.py
  - tests/services/test_check.py
  - tests/actions/test_core_registrations.py
autonomous: true
requirements: [POLR-04]

must_haves:
  truths:
    - "check_alignment action accepts a decision description and returns structured polaris context"
    - "Result includes aligned bool, relevant_priorities list, and reasoning string"
    - "check_alignment is registered in ActionRegistry and auto-generates CLI/MCP surfaces"
  artifacts:
    - path: "src/ztlctl/actions/_check.py"
      provides: "check_alignment ActionDefinition registration"
      contains: "check_alignment"
    - path: "src/ztlctl/services/check.py"
      provides: "check_alignment service method"
      contains: "def check_alignment"
    - path: "src/ztlctl/controllers/check.py"
      provides: "check_alignment controller method delegating to service"
      contains: "check_alignment"
  key_links:
    - from: "src/ztlctl/actions/_check.py"
      to: "src/ztlctl/controllers/check.py"
      via: "ActionDefinition handler lambda"
      pattern: "CheckController.*check_alignment"
    - from: "src/ztlctl/controllers/check.py"
      to: "src/ztlctl/services/check.py"
      via: "controller delegates to service via _run_action"
      pattern: "CheckService.*check_alignment"
    - from: "src/ztlctl/services/check.py"
      to: "garden/groves/polaris.md"
      via: "reads polaris file to extract priorities"
      pattern: "polaris"
---

<objective>
Implement the check_alignment action that accepts a decision description and returns structured polaris context for agent evaluation.

Purpose: Enable agents to programmatically check whether a proposed decision aligns with the user's polaris priorities, providing structured reasoning data.
Output: check_alignment service method, controller method, ActionDefinition registration, tests.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/19-methodology-guidance-and-polaris/19-CONTEXT.md

<interfaces>
<!-- Key types and contracts the executor needs. -->

From src/ztlctl/actions/_check.py (registration pattern):
```python
def _register_check_actions() -> None:
    from ztlctl.actions.definitions import ActionDefinition, ActionParam
    from ztlctl.actions.registry import get_action_registry
    from ztlctl.controllers.check import CheckController
    registry = get_action_registry()
    registry.register(ActionDefinition(
        name="check",
        description="Report integrity issues without modifying anything.",
        category="check",
        params=(ActionParam("min_severity", str, ...),),
        handler=lambda vault, **kw: CheckController(vault).check(**kw),
        side_effect="read",
        mcp_when_to_use="...",
        mcp_avoid_when="...",
        cli_group="check",
    ))
```

From src/ztlctl/controllers/check.py (controller pattern, uses _run_action):
```python
class CheckController(BaseController):
    def check(self, *, min_severity: str = "warning") -> ServiceResult:
        from ztlctl.services.check import CheckService
        kwargs = {"min_severity": min_severity}
        def _invoke(kw): return CheckService(self._vault).check(**kw)
        return self._run_action("check", kwargs, _invoke)
```

From src/ztlctl/services/result.py:
```python
class ServiceResult(BaseModel):
    ok: bool
    op: str
    data: dict[str, Any] = {}
    error: ServiceError | None = None
    warnings: list[str] = []
```

From CONTEXT.md decisions:
- check_alignment returns ServiceResult with data: {aligned: bool, relevant_priorities: [...], reasoning: str}
- Polaris document lives at garden/groves/polaris.md
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: check_alignment service method + controller + action registration</name>
  <files>
    src/ztlctl/services/check.py
    src/ztlctl/controllers/check.py
    src/ztlctl/actions/_check.py
    tests/services/test_check.py
    tests/actions/test_core_registrations.py
  </files>
  <read_first>
    src/ztlctl/services/check.py
    src/ztlctl/controllers/check.py
    src/ztlctl/actions/_check.py
    tests/services/test_check.py
    tests/actions/test_core_registrations.py
    src/ztlctl/controllers/base.py
    src/ztlctl/domain/content.py
  </read_first>
  <behavior>
    - Test: check_alignment with a decision description returns ServiceResult with ok=True
    - Test: result data contains "aligned" (bool), "relevant_priorities" (list), "reasoning" (str)
    - Test: when polaris file exists with priorities, relevant_priorities contains matching entries
    - Test: when polaris file does not exist, result has ok=True with aligned=True, empty priorities, and reasoning explaining no polaris configured
    - Test: check_alignment action is registered in ActionRegistry with name "check_alignment"
    - Test: action has "decision" param (required str) and category "check"
  </behavior>
  <action>
    1. Add `check_alignment` method to `CheckService` in `src/ztlctl/services/check.py`:
       ```python
       @traced
       def check_alignment(self, *, decision: str) -> ServiceResult:
           """Check a decision description against polaris priorities.

           Returns structured data: {aligned, relevant_priorities, reasoning}.
           Alignment is heuristic — keyword overlap between decision and priorities.
           """
           polaris_path = self._vault.root / "garden" / "groves" / "polaris.md"
           if not polaris_path.exists():
               return ServiceResult(
                   ok=True,
                   op="check_alignment",
                   data={
                       "aligned": True,
                       "relevant_priorities": [],
                       "reasoning": "No polaris file configured. Create garden/groves/polaris.md or run ztlctl init.",
                       "polaris_exists": False,
                   },
               )

           polaris_raw = polaris_path.read_text(encoding="utf-8")
           fm, body = parse_frontmatter(polaris_raw)

           # Extract priority lines (numbered items under "Current Priorities")
           priorities: list[str] = []
           in_priorities = False
           for line in body.splitlines():
               stripped = line.strip()
               if stripped.lower().startswith("## current priorities"):
                   in_priorities = True
                   continue
               if in_priorities and stripped.startswith("##"):
                   break
               if in_priorities and stripped and (stripped[0].isdigit() or stripped.startswith("-")):
                   # Strip leading number/bullet
                   text = stripped.lstrip("0123456789.-) ").strip()
                   if text:
                       priorities.append(text)

           # Extract decision principles
           principles: list[str] = []
           in_principles = False
           for line in body.splitlines():
               stripped = line.strip()
               if stripped.lower().startswith("## decision principles"):
                   in_principles = True
                   continue
               if in_principles and stripped.startswith("##"):
                   break
               if in_principles and stripped and stripped.startswith("-"):
                   text = stripped.lstrip("- ").strip()
                   if text:
                       principles.append(text)

           # Heuristic relevance: keyword overlap between decision and each priority/principle
           decision_words = set(decision.lower().split())
           relevant: list[str] = []
           for p in priorities + principles:
               p_words = set(p.lower().split())
               overlap = decision_words & p_words - {"the", "a", "an", "is", "to", "and", "or", "of", "in", "for", "with", "on", "at", "by"}
               if overlap:
                   relevant.append(p)

           if relevant:
               reasoning = f"Decision relates to {len(relevant)} polaris priorities/principles."
           else:
               reasoning = "No direct keyword overlap found between the decision and current polaris priorities."

           return ServiceResult(
               ok=True,
               op="check_alignment",
               data={
                   "aligned": True,  # Advisory — never blocks
                   "relevant_priorities": relevant,
                   "reasoning": reasoning,
                   "polaris_exists": True,
                   "decision": decision,
                   "all_priorities": priorities,
                   "all_principles": principles,
               },
           )
       ```

    2. Add `check_alignment` method to `CheckController` in `src/ztlctl/controllers/check.py`:
       ```python
       def check_alignment(self, *, decision: str) -> ServiceResult:
           """Check decision alignment against polaris priorities."""
           from ztlctl.services.check import CheckService

           kwargs: dict[str, Any] = {"decision": decision}

           def _invoke(kw: dict[str, Any]) -> ServiceResult:
               return CheckService(self._vault).check_alignment(**kw)

           return self._run_action("check_alignment", kwargs, _invoke)
       ```

    3. Register `check_alignment` action in `src/ztlctl/actions/_check.py` (append to `_register_check_actions`):
       ```python
       registry.register(
           ActionDefinition(
               name="check_alignment",
               description="Check a decision against polaris priorities for alignment.",
               category="check",
               params=(
                   ActionParam(
                       "decision",
                       str,
                       required=True,
                       description="Description of the decision to evaluate against polaris priorities.",
                   ),
               ),
               handler=lambda vault, **kw: CheckController(vault).check_alignment(**kw),
               side_effect="read",
               mcp_when_to_use="Before making a significant decision, to check alignment with the vault's polaris priorities.",
               mcp_avoid_when="No polaris file exists yet, or the decision is trivial.",
               cli_group="check",
               cli_name="alignment",
           )
       )
       ```

    4. Add tests in `tests/services/test_check.py`:
       - Test with polaris file containing priorities: create `garden/groves/polaris.md` with numbered priorities and decision principles, call `check_alignment(decision="...")`, verify result shape
       - Test with keyword overlap: decision mentioning a priority keyword has that priority in `relevant_priorities`
       - Test without polaris file: `polaris_exists` is False, `aligned` is True, empty `relevant_priorities`
       - Test result data structure: `aligned` is bool, `relevant_priorities` is list, `reasoning` is str

    5. Verify `check_alignment` appears in `tests/actions/test_core_registrations.py` (the existing test that checks all registered actions should pick it up automatically if it iterates the registry).
  </action>
  <verify>
    <automated>uv run pytest tests/services/test_check.py -x -q -k alignment && uv run pytest tests/actions/test_core_registrations.py -x -q</automated>
  </verify>
  <acceptance_criteria>
    - grep "check_alignment" src/ztlctl/services/check.py finds the service method
    - grep "check_alignment" src/ztlctl/controllers/check.py finds the controller method
    - grep "check_alignment" src/ztlctl/actions/_check.py finds the ActionDefinition registration
    - grep "aligned" src/ztlctl/services/check.py finds the result field
    - grep "relevant_priorities" src/ztlctl/services/check.py finds the result field
    - grep "reasoning" src/ztlctl/services/check.py finds the result field
    - grep "alignment" tests/services/test_check.py finds test coverage
  </acceptance_criteria>
  <done>
    - check_alignment service method reads polaris, extracts priorities, returns structured result
    - Controller delegates via _run_action pattern
    - ActionDefinition registered under "check" category with cli_name="alignment"
    - Result data has: aligned (bool), relevant_priorities (list), reasoning (str)
    - Auto-generates CLI (`ztlctl check alignment`) and MCP (`check_alignment` tool) surfaces
    - All tests pass
  </done>
</task>

</tasks>

<verification>
1. `uv run pytest tests/services/test_check.py tests/actions/test_core_registrations.py -x -q` — all tests pass
2. `uv run ruff check src/ztlctl/services/check.py src/ztlctl/controllers/check.py src/ztlctl/actions/_check.py` — no lint errors
3. `uv run mypy src/ztlctl/services/check.py src/ztlctl/controllers/check.py src/ztlctl/actions/_check.py` — no type errors
4. `uv run pytest -x -q` — full test suite passes (no regressions)
</verification>

<success_criteria>
- check_alignment action registered in ActionRegistry
- Service method reads polaris, extracts priorities/principles, returns structured result
- Agent can call check_alignment with a decision description and receive alignment context
- Result always has aligned=True (advisory, never blocks)
- All tests pass, lint clean, types clean
</success_criteria>

<output>
After completion, create `.planning/phases/19-methodology-guidance-and-polaris/19-03-SUMMARY.md`
</output>
