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
Summary
pact-plugin/hooks/wake_lifecycle_emitter.py::_extract_task_iddoes not match the actual platformPostToolUsepayload shape forTaskCreate. 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-inboxand the SessionStart-resume path still work; only the hook-driven 0→1 Arm is broken.Empirical Evidence
Captured live
PostToolUsestdin in sessionpact-56ce3a2aon 2026-05-02 by replacing the hook with a logging shim and triggering a realTaskCreate:{ "tool_name": "TaskCreate", "tool_input": {"subject": "...", "description": "..."}, "tool_response": {"task": {"id": "5", "subject": "..."}} }tool_response.task.id— id is nested undertask.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 attool_response.task.idResult: returns
None→_decide_directiveexits at theif not _extract_task_id(...): return Noneguard →main()prints{\"suppressOutput\": true}. Never emits the Arm directive on TaskCreate.TaskUpdateis unaffected — its productiontool_inputhastaskIddirectly, so the Teardown path works.Why Tests Pass
pact-plugin/tests/test_inbox_wake_lifecycle_emitter.pyfixture uses: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_idto also probe nested shapes:Add a regression test that pipes the captured production payload (
tool_response: {\"task\": {\"id\": \"...\"}}) end-to-end and assertsadditionalContextis emitted on the 0→1 transition.Impact
/PACT:watch-inboxinvocation remains the recovery path.References