Skip to content

wake_lifecycle_emitter: Arm directive never emits — _extract_task_id misses production tool_response shape #612

Description

@michael-wojcik

Summary

pact-plugin/hooks/wake_lifecycle_emitter.py::_extract_task_id does not match the actual platform PostToolUse payload shape for TaskCreate. The hook silently no-ops on every first-active-task transition, so the auto-Arm flow shipped in #603 has never fired in production. Manual /PACT:watch-inbox and the SessionStart-resume path still work; only the hook-driven 0→1 Arm is broken.

Empirical Evidence

Captured live PostToolUse stdin in session pact-56ce3a2a on 2026-05-02 by replacing the hook with a logging shim and triggering a real TaskCreate:

{
  "tool_name": "TaskCreate",
  "tool_input": {"subject": "...", "description": "..."},
  "tool_response": {"task": {"id": "5", "subject": "..."}}
}

tool_response.task.id — id is nested under task.

Bug

_extract_task_id (lines ~145-167) probes:

  • tool_input.taskId / tool_input.task_id — TaskCreate has neither (id is platform-assigned)
  • tool_response.id / tool_response.taskId / tool_response.task_id — production puts it at tool_response.task.id

Result: returns None → _decide_directive exits at the if not _extract_task_id(...): return None guard → main() prints {\"suppressOutput\": true}. Never emits the Arm directive on TaskCreate.

TaskUpdate is unaffected — its production tool_input has taskId directly, so the Teardown path works.

Why Tests Pass

pact-plugin/tests/test_inbox_wake_lifecycle_emitter.py fixture uses:

\"tool_input\": {\"taskId\": \"task-1\"},
\"tool_response\": {\"id\": \"task-1\"},

This shape is incorrect for TaskCreate. Tests are green; production silently fails. See companion issue for the test-fixture-parity problem.

Proposed Fix

Extend _extract_task_id to also probe nested shapes:

tool_response = input_data.get(\"tool_response\") or {}
if isinstance(tool_response, dict):
    # nested-task shape (TaskCreate production)
    task = tool_response.get(\"task\")
    if isinstance(task, dict):
        tid = task.get(\"id\") or task.get(\"taskId\") or task.get(\"task_id\")
        if isinstance(tid, str) and tid:
            return tid
    # flat shape (TaskUpdate production + legacy fixtures)
    tid = tool_response.get(\"id\") or tool_response.get(\"taskId\") or tool_response.get(\"task_id\")
    if isinstance(tid, str) and tid:
        return tid

Add a regression test that pipes the captured production payload (tool_response: {\"task\": {\"id\": \"...\"}}) end-to-end and asserts additionalContext is emitted on the 0→1 transition.

Impact

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions