---
phase: 07-plugin-agentic-wiring-fixes
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
  - src/ztlctl/controllers/query.py
  - src/ztlctl/controllers/reweave.py
  - src/ztlctl/controllers/session.py
  - src/ztlctl/controllers/update.py
  - src/ztlctl/controllers/upgrade.py
  - src/ztlctl/controllers/vector.py
  - src/ztlctl/controllers/workflow.py
  - tests/controllers/test_hook_wiring_batch2.py
autonomous: true
requirements: [PLUG-02]

must_haves:
  truths:
    - "Every controller method in batch 2 calls _dispatch_pre_action before service delegation"
    - "A plugin returning ActionRejection prevents service execution and returns a ServiceResult error"
    - "_dispatch_post_action fires after every successful service call regardless of result.ok"
    - "WorkflowController methods returning non-ServiceResult types (read_answers, profile_choices, default_choices) are NOT wired with hooks"
    - "VectorController.status uses action name 'vector_status' (not 'status') to avoid collision with SessionController"
  artifacts:
    - path: "src/ztlctl/controllers/query.py"
      provides: "Hook-wired count_items, search, get, list_items, work_queue, list_tags, decision_support, topic_packet, draft_from_topic, vault_review"
      contains: "_dispatch_pre_action"
    - path: "src/ztlctl/controllers/reweave.py"
      provides: "Hook-wired reweave, prune, undo"
      contains: "_dispatch_pre_action"
    - path: "src/ztlctl/controllers/session.py"
      provides: "Hook-wired start, close, reopen, status, log_entry, cost, context, brief, extract_decision"
      contains: "_dispatch_pre_action"
    - path: "src/ztlctl/controllers/update.py"
      provides: "Hook-wired update, archive, supersede"
      contains: "_dispatch_pre_action"
    - path: "src/ztlctl/controllers/upgrade.py"
      provides: "Hook-wired check_pending, apply, stamp_current"
      contains: "_dispatch_pre_action"
    - path: "src/ztlctl/controllers/vector.py"
      provides: "Hook-wired status (vector_status), reindex_all"
      contains: "_dispatch_pre_action"
    - path: "src/ztlctl/controllers/workflow.py"
      provides: "Hook-wired init_workflow, update_workflow, export_assets, validate_assets; read_answers/profile_choices/default_choices left unwired"
      contains: "_dispatch_pre_action"
    - path: "tests/controllers/test_hook_wiring_batch2.py"
      provides: "Spot-check tests for batch 2 controller hook wiring"
  key_links:
    - from: "src/ztlctl/controllers/*.py"
      to: "src/ztlctl/controllers/base.py"
      via: "_dispatch_pre_action and _dispatch_post_action inherited methods"
      pattern: "_dispatch_pre_action"
---

<objective>
Wire pre/post-action hooks into the remaining 7 controllers (query, reweave, session, update, upgrade, vector, workflow) — 33 ServiceResult methods total. Skip 3 non-ServiceResult WorkflowController helper methods.

Purpose: Completes PLUG-02 by wiring hooks into all remaining controllers, achieving systematic coverage of all 59+ controller methods that return ServiceResult.
Output: 7 updated controller files + hook wiring spot-check tests.
</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/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/07-plugin-agentic-wiring-fixes/07-CONTEXT.md
@.planning/phases/07-plugin-agentic-wiring-fixes/07-RESEARCH.md

<interfaces>
<!-- BaseController hook dispatch methods (inherited by all controllers) -->

From src/ztlctl/controllers/base.py:
```python
class BaseController:
    def _dispatch_pre_action(
        self, action_name: str, kwargs: dict[str, Any],
    ) -> tuple[dict[str, Any], ActionRejection | None]: ...

    def _dispatch_post_action(
        self, action_name: str, kwargs: dict[str, Any], result: Any,
    ) -> None: ...
```

From src/ztlctl/plugins/contracts.py:
```python
@dataclass(frozen=True)
class ActionRejection:
    reason: str
    code: str = ""
    detail: dict[str, Any] = field(default_factory=dict)
```

From src/ztlctl/services/result.py:
```python
class ServiceError(BaseModel):
    code: str
    message: str
    detail: dict[str, Any] = Field(default_factory=dict)
    recovery: str | None = None

class ServiceResult(BaseModel):
    ok: bool
    op: str
    data: dict[str, Any] = Field(default_factory=dict)
    warnings: list[str] | None = None
    error: ServiceError | None = None
    meta: dict[str, Any] | None = None
```

<!-- Action names from _register_core.py (authoritative) -->
QueryController: "count_items", "search", "get", "list_items", "work_queue", "list_tags", "decision_support", "topic_packet", "draft_from_topic", "vault_review"
ReweaveController: "reweave", "prune", "undo"
SessionController: "start", "close", "reopen", "status", "log_entry", "cost", "context", "brief", "extract_decision"
UpdateController: "update", "archive", "supersede"
UpgradeController: "check_pending", "apply", "stamp_current"
VectorController: "vector_status", "reindex_all"
WorkflowController (ServiceResult methods only): "init_workflow", "update_workflow", "export_assets"
WorkflowController (skip — non-ServiceResult): read_answers, profile_choices, default_choices
Note: validate_assets has no ActionDefinition in _register_core.py — use "validate_assets" as action name anyway for consistency.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Wire hooks into 7 controllers (query, reweave, session, update, upgrade, vector, workflow)</name>
  <files>src/ztlctl/controllers/query.py, src/ztlctl/controllers/reweave.py, src/ztlctl/controllers/session.py, src/ztlctl/controllers/update.py, src/ztlctl/controllers/upgrade.py, src/ztlctl/controllers/vector.py, src/ztlctl/controllers/workflow.py</files>
  <read_first>
    - src/ztlctl/controllers/base.py (full — _dispatch_pre_action and _dispatch_post_action signatures)
    - src/ztlctl/controllers/query.py (full — current method bodies)
    - src/ztlctl/controllers/reweave.py (full — current method bodies)
    - src/ztlctl/controllers/session.py (full — current method bodies)
    - src/ztlctl/controllers/update.py (full — current method bodies)
    - src/ztlctl/controllers/upgrade.py (full — current method bodies)
    - src/ztlctl/controllers/vector.py (full — current method bodies, note inline ServiceResult in status())
    - src/ztlctl/controllers/workflow.py (full — identify ServiceResult vs non-ServiceResult methods)
    - src/ztlctl/plugins/contracts.py (ActionRejection dataclass)
  </read_first>
  <action>
Apply the same 4-step pattern from Plan 02 to all ServiceResult-returning methods in these 7 controllers. Same rules apply: add `from typing import Any` if missing, add `from ztlctl.services.result import ServiceError, ServiceResult` as lazy import inside each method, use `kwargs["param"]` for all service call arguments.

**QueryController (10 methods):**
- `count_items`: kwargs = `{"include_archived": include_archived}`, action = `"count_items"`
- `search`: kwargs = `{"query": query, "content_type": content_type, "tag": tag, "space": space, "rank_by": rank_by, "limit": limit}`, action = `"search"`. Pass `kwargs["query"]` positionally.
- `get`: kwargs = `{"content_id": content_id}`, action = `"get"`. Pass `kwargs["content_id"]` positionally.
- `list_items`: kwargs = `{"content_type": content_type, "status": status, "tag": tag, "topic": topic, "subtype": subtype, "maturity": maturity, "space": space, "since": since, "include_archived": include_archived, "sort": sort, "limit": limit}`, action = `"list_items"`
- `work_queue`: kwargs = `{"space": space}`, action = `"work_queue"`
- `list_tags`: kwargs = `{"prefix": prefix, "limit": limit}`, action = `"list_tags"`
- `decision_support`: kwargs = `{"topic": topic, "space": space}`, action = `"decision_support"`
- `topic_packet`: kwargs = `{"topic": topic, "mode": mode, "budget": budget}`, action = `"topic_packet"`. Pass `kwargs["topic"]` positionally.
- `draft_from_topic`: kwargs = `{"topic": topic, "target": target, "mode": mode, "budget": budget}`, action = `"draft_from_topic"`. Pass `kwargs["topic"]` positionally.
- `vault_review`: kwargs = `{"top": top, "stale_days": stale_days}`, action = `"vault_review"`

**ReweaveController (3 methods):**
- `reweave`: kwargs = `{"content_id": content_id, "dry_run": dry_run, "min_score_override": min_score_override}`, action = `"reweave"`
- `prune`: kwargs = `{"content_id": content_id, "dry_run": dry_run}`, action = `"prune"`
- `undo`: kwargs = `{"reweave_id": reweave_id}`, action = `"undo"`

**SessionController (9 methods):**
- `start`: kwargs = `{"topic": topic}`, action = `"start"`. Pass `kwargs["topic"]` positionally.
- `close`: kwargs = `{"summary": summary}`, action = `"close"`
- `reopen`: kwargs = `{"session_id": session_id}`, action = `"reopen"`. Pass `kwargs["session_id"]` positionally.
- `status`: kwargs = `{}`, action = `"status"`
- `log_entry`: kwargs = `{"message": message, "pin": pin, "cost": cost, "detail": detail, "entry_type": entry_type, "subtype": subtype, "references": references, "metadata": metadata}`, action = `"log_entry"`. Pass `kwargs["message"]` positionally.
- `cost`: kwargs = `{"report": report}`, action = `"cost"`
- `context`: kwargs = `{"topic": topic, "budget": budget, "ignore_checkpoints": ignore_checkpoints}`, action = `"context"`
- `brief`: kwargs = `{}`, action = `"brief"`
- `extract_decision`: kwargs = `{"session_id": session_id, "title": title}`, action = `"extract_decision"`. Pass `kwargs["session_id"]` positionally.

**UpdateController (3 methods):**
- `update`: kwargs = `{"content_id": content_id, "changes": changes}`, action = `"update"`. Pass `kwargs["content_id"]` positionally.
- `archive`: kwargs = `{"content_id": content_id}`, action = `"archive"`. Pass `kwargs["content_id"]` positionally.
- `supersede`: kwargs = `{"old_id": old_id, "new_id": new_id}`, action = `"supersede"`. Pass `kwargs["old_id"]`, `kwargs["new_id"]` positionally.

**UpgradeController (3 methods):**
- `check_pending`: kwargs = `{}`, action = `"check_pending"`
- `apply`: kwargs = `{}`, action = `"apply"`
- `stamp_current`: kwargs = `{}`, action = `"stamp_current"`

**VectorController (2 methods):**
- `status`: kwargs = `{}`, action = `"vector_status"` (NOT `"status"` — the ActionDefinition name is `"vector_status"`). The method constructs ServiceResult inline (like DiscoveryController). Wrap the inline body after rejection check. The `from ztlctl.services.result import ServiceResult` import that already exists in the method body needs `ServiceError` added.
- `reindex_all`: kwargs = `{}`, action = `"reindex_all"`

**WorkflowController (4 ServiceResult methods, 3 skipped):**
- `init_workflow`: kwargs = `{"vault_root": vault_root, "choices": choices, "force_trust": force_trust}`, action = `"init_workflow"`
- `update_workflow`: kwargs = `{"vault_root": vault_root, "choices": choices, "force_trust": force_trust}`, action = `"update_workflow"`
- `export_assets`: kwargs = `{"vault_root": vault_root, "client": client}`, action = `"export_assets"`
- `validate_assets`: kwargs = `{"vault_root": vault_root, "client": client}`, action = `"validate_assets"` (no ActionDefinition exists, but wire for completeness)
- `read_answers`, `profile_choices`, `default_choices`: DO NOT WIRE — they return non-ServiceResult types
  </action>
  <verify>
    <automated>cd /Users/shparki/Documents/Workspace/thatdev/ztlctl && for f in query reweave session update upgrade vector workflow; do echo "=== $f ===" && grep -c "_dispatch_pre_action" src/ztlctl/controllers/${f}.py; done && uv run ruff check src/ztlctl/controllers/query.py src/ztlctl/controllers/reweave.py src/ztlctl/controllers/session.py src/ztlctl/controllers/update.py src/ztlctl/controllers/upgrade.py src/ztlctl/controllers/vector.py src/ztlctl/controllers/workflow.py && uv run mypy src/ztlctl/controllers/query.py src/ztlctl/controllers/reweave.py src/ztlctl/controllers/session.py src/ztlctl/controllers/update.py src/ztlctl/controllers/upgrade.py src/ztlctl/controllers/vector.py src/ztlctl/controllers/workflow.py</automated>
  </verify>
  <acceptance_criteria>
    - query.py: `grep -c "_dispatch_pre_action" src/ztlctl/controllers/query.py` returns 10
    - reweave.py: `grep -c "_dispatch_pre_action" src/ztlctl/controllers/reweave.py` returns 3
    - session.py: `grep -c "_dispatch_pre_action" src/ztlctl/controllers/session.py` returns 9
    - update.py: `grep -c "_dispatch_pre_action" src/ztlctl/controllers/update.py` returns 3
    - upgrade.py: `grep -c "_dispatch_pre_action" src/ztlctl/controllers/upgrade.py` returns 3
    - vector.py: `grep -c "_dispatch_pre_action" src/ztlctl/controllers/vector.py` returns 2
    - workflow.py: `grep -c "_dispatch_pre_action" src/ztlctl/controllers/workflow.py` returns 4
    - workflow.py read_answers, profile_choices, default_choices do NOT contain `_dispatch_pre_action`
    - vector.py contains `"vector_status"` as action name (not `"status"`)
    - `uv run ruff check` on all 7 files exits 0
    - `uv run mypy` on all 7 files exits 0
  </acceptance_criteria>
  <done>33 ServiceResult methods across 7 controllers wired with pre/post-action hooks; 3 non-ServiceResult WorkflowController methods correctly skipped; ruff and mypy clean</done>
</task>

<task type="auto">
  <name>Task 2: Spot-check tests + full regression for batch 2 controllers</name>
  <files>tests/controllers/test_hook_wiring_batch2.py</files>
  <read_first>
    - tests/controllers/test_hook_wiring.py (batch 1 test patterns from Plan 02)
    - tests/controllers/test_query.py (existing query controller test patterns)
    - tests/controllers/test_session.py (existing session controller test patterns)
    - src/ztlctl/controllers/query.py (after Task 1 wiring)
    - src/ztlctl/controllers/session.py (after Task 1 wiring)
    - src/ztlctl/controllers/vector.py (after Task 1 wiring — vector_status action name)
    - src/ztlctl/controllers/workflow.py (after Task 1 wiring — skip methods)
  </read_first>
  <action>
Create `tests/controllers/test_hook_wiring_batch2.py` following the same pattern as test_hook_wiring.py from Plan 02.

**Test cases (one per controller, 7 tests + 2 special):**

1. `test_query_controller_search_dispatches_hooks()` — QueryController.search("test query")
   - Assert `_dispatch_pre_action` called with `("search", {"query": "test query", "content_type": None, "tag": None, "space": None, "rank_by": "relevance", "limit": 20})`

2. `test_reweave_controller_reweave_dispatches_hooks()` — ReweaveController.reweave(content_id="ZTL-001")
   - Assert `_dispatch_pre_action` called with `("reweave", {"content_id": "ZTL-001", "dry_run": False, "min_score_override": None})`

3. `test_session_controller_start_dispatches_hooks()` — SessionController.start("my topic")
   - Assert `_dispatch_pre_action` called with `("start", {"topic": "my topic"})`

4. `test_update_controller_update_dispatches_hooks()` — UpdateController.update("ZTL-001", changes={"title": "new"})
   - Assert `_dispatch_pre_action` called with `("update", {"content_id": "ZTL-001", "changes": {"title": "new"}})`

5. `test_upgrade_controller_apply_dispatches_hooks()` — UpgradeController.apply()
   - Assert `_dispatch_pre_action` called with `("apply", {})`

6. `test_vector_controller_status_uses_vector_status_action()` — VectorController.status()
   - Assert `_dispatch_pre_action` called with `("vector_status", {})` — NOT `("status", {})`

7. `test_workflow_controller_init_dispatches_hooks()` — WorkflowController.init_workflow(Path("/tmp"), choices=mock_choices)
   - Assert `_dispatch_pre_action` called with `("init_workflow", {...})`

**Special test cases:**

8. `test_workflow_controller_skip_non_service_result_methods()` — Verify that read_answers, profile_choices, default_choices do NOT call `_dispatch_pre_action`.
   - Create WorkflowController with mock vault
   - Patch `_dispatch_pre_action` on the instance
   - Call read_answers, profile_choices, default_choices (mocking service layer)
   - Assert `_dispatch_pre_action` was NOT called

9. `test_session_controller_log_entry_kwargs_complete()` — Verify log_entry passes all 8 kwargs.
   - Call log_entry with message="test", pin=True, cost=100
   - Assert kwargs dict contains all 8 keys: message, pin, cost, detail, entry_type, subtype, references, metadata
  </action>
  <verify>
    <automated>cd /Users/shparki/Documents/Workspace/thatdev/ztlctl && uv run pytest tests/controllers/test_hook_wiring_batch2.py -x -q && uv run pytest tests/controllers/ -x -q</automated>
  </verify>
  <acceptance_criteria>
    - tests/controllers/test_hook_wiring_batch2.py exists with at least 9 test functions
    - All tests pass
    - test_vector_controller_status_uses_vector_status_action asserts action name is "vector_status"
    - test_workflow_controller_skip_non_service_result_methods asserts _dispatch_pre_action was NOT called for helper methods
    - `uv run pytest tests/controllers/ -x` exits 0 (all existing + new tests)
  </acceptance_criteria>
  <done>9 spot-check tests confirm hook wiring for batch 2 controllers, including vector_status action name, workflow skip behavior, and log_entry completeness; full controller test suite green</done>
</task>

</tasks>

<verification>
- All 7 controller files have _dispatch_pre_action calls matching their ServiceResult method count
- `uv run ruff check src/ztlctl/controllers/` clean
- `uv run mypy src/ztlctl/controllers/` clean
- `uv run pytest tests/controllers/ -x -q` all tests pass
- `uv run pytest` full suite green (no regressions across entire codebase)
</verification>

<success_criteria>
- 33 ServiceResult methods across 7 controllers wired with pre/post-action hooks
- 3 non-ServiceResult WorkflowController methods correctly left unwired
- vector_status action name used for VectorController.status() (not "status")
- Action names match _register_core.py exactly for all methods with ActionDefinitions
- All hook wiring tests pass
- Full test suite green
</success_criteria>

<output>
After completion, create `.planning/phases/07-plugin-agentic-wiring-fixes/07-03-SUMMARY.md`
</output>
