--- name: Syncro — timer_entry response is FLAT, not wrapped description: POST /tickets/{id}/timer_entry returns a flat object {"id": N, "ticket_id": ..., "product_id": ..., ...}, NOT wrapped in {"timer": {...}} or {"timer_entry": {...}}. Parse as `.id`, never `.timer.id` — using the wrapped pattern silently returns null and creates duplicate timers when the script "retries". type: feedback --- **Rule:** When parsing the response from `POST /tickets/{id}/timer_entry`, use `.id` directly — the response is a FLAT object. Do NOT use `.timer.id // .timer_entry.id`. **Verified response shape (2026-05-05, ticket #32253):** ```json { "id": 39031258, "ticket_id": 109895882, "user_id": 1750, "start_time": "2026-05-05T09:00:00.000-07:00", "end_time": "2026-05-05T09:30:00.000-07:00", "recorded": false, "billable": true, "notes": "...", "product_id": 26118, "comment_id": null, "ticket_line_item_id": null, "active_duration": 1800, "billable_time": 1800 ... } ``` **Why:** The skill doc at `.claude/commands/syncro.md` shows ```bash TIMER_ID=$(echo "$TIMER_RESP" | jq -r '.timer.id // .timer_entry.id') ``` That fallback resolves to `null` because neither key exists on the flat response. A `null` TIMER_ID then breaks `charge_timer_entry` ("Not found"). If the script retries the timer_entry POST after the perceived failure, it creates a duplicate — Syncro has no idempotency. Hit this on ticket #32253 (Cascades) on 2026-05-05; created two duplicate 0.5hr timers and had to delete one via `delete_timer_entry` before charging. **How to apply:** - **Parsing:** Always `jq -r '.id'` on the timer_entry response. - **After ANY ambiguous timer_entry response** (null `.id`, jq error, network blip): GET the ticket and inspect `.ticket.ticket_timers[]` BEFORE retrying. Filter for `recorded: false` entries with the start/end times you just sent. - **Cleanup if duplicates exist:** `POST /tickets/{id}/delete_timer_entry` with `{"timer_entry_id": N}` for the older duplicate(s). Returns `{"success": true}`. - **Verifying the timer is on the ticket:** `GET /tickets/{id}` → `.ticket.ticket_timers` is the authoritative list. The standalone `/ticket_timers?ticket_id=N` query parameter does NOT filter by ticket — returns the entire global timer history. **Charge timer response is also flat:** ```json {"id": 39031258, "recorded": true, "ticket_line_item_id": 42313052, ...} ``` Parse as `.ticket_line_item_id` to get the auto-generated line. Do not look for a wrapper. **Where this lands in skill code:** `.claude/commands/syncro.md` example block needs `.id` not `.timer.id // .timer_entry.id`. Until the skill is patched, override the example pattern when running.