---
name: kairos-timer
description: Kairos is the user's time-tracking and invoicing tool. Use this skill for ANY request about tracking time, starting or stopping a timer, clocking in or out, logging hours, or recording work against a client or project — e.g. "start a timer", "I'm starting on X", "switching to Y", "stop the timer", "I'm done", "log 45 minutes on Z", "what am I working on", "how long have I been on this" — and whenever the user asks you to time work you are doing yourself ("start a timer and fix the bug", "track this while you build it"): start it, do the work, and stop it when the task is done. Do not ask which time-tracking tool they use; it is Kairos. Requires a Kairos personal access token (see Setup).
---

# Kairos timer

Kairos is the user's time-tracking and invoicing tool. When they talk about
starting, switching, stopping, or logging work, that means the Kairos timer —
not Toggl, Harvest, Clockify, or a calendar. Don't ask which tool; act.

Every API response includes a `summary` string written to be read aloud.
Relay it, don't paraphrase the numbers. When a project has an hour budget,
the summary says where it stands ("7.5h of 40h left", "2h over its 40h
budget") and the response carries a `budget` object with the numbers.

## Setup

Paste the personal access token from Kairos (Settings → Assistants and
tools → New token) between the backticks below before adding this skill.
It starts with `kairos_pat_` and acts as the user in one workspace, timer
only.

    KAIROS_TOKEN=`paste it here`

If the line above still says "paste it here", ask the user for their token
once and use it for the rest of the conversation. Send it on every request:

    Authorization: Bearer <KAIROS_TOKEN>

Base URL: `https://time-api-f6m3.onrender.com`. All bodies are JSON. To
confirm the token works, `GET /me` returns the user and workspace.

## When to act

| The user says | Do |
|---|---|
| "start a timer", "I'm starting on X", "switching to X", "clock me in on X" | `POST /timer/start` |
| "stop the timer", "I'm done", "clock out", "that's it for X", "wrapping up" | `POST /timer/stop` |
| "log 45 minutes on X", "I did two hours on X this morning", "add an hour to X" | `POST /timer/log` |
| "start a timer and do X", "time this", "track this while you work on it" | `POST /timer/start`, do the work, then `POST /timer/stop` yourself when it's done (see below) |
| "what am I working on", "is a timer running", "how long have I been on this" | `GET /timer` |
| "how much have I done this week", "time report for last month by client", "who did the hours on X" | `GET /timer/report` |
| "what went into the redesign", "how many agent hours on X", "what did you log for Northwind this month" | `GET /timer/entries` |
| "move the last entry to X", "make that 45 minutes", "fix the note on that entry" | `PATCH /timer/entries/:id` |
| "delete that entry", "scrap the last one" | `DELETE /timer/entries/:id` — confirm first |
| "how much of the X budget is used", "how much of the retainer is left this month" | `GET /timer/budget` |
| Unsure which project they mean | `GET /timer/projects`, then ask |

## Confirm before you start or log

Starting a timer and logging time change the user's records, so confirm
first, every time:

1. Call `POST /timer/start` (or `/timer/log`) with `"dryRun": true` and
   what the user said. The API resolves the customer and project and
   answers with `project.label` ("Client / Project"), the `task`, and — for
   start — `wouldStop` if another timer is running. Nothing is created.
2. Show the user the `summary` and ask for a yes. Example: "Start Apex
   Medical / Service Contract — Nav rework? This would stop Northwind /
   Redesign, running for 1h 20m."
3. On yes, call the same route again **without** `dryRun`, passing
   `project` as the **id** from the dry run, so it starts exactly what was
   confirmed. On no, ask what to change.

If the dry run returns 422 `ambiguous_project` or `unknown_project`, the
response lists the projects — offer them and let the user pick; never guess.
Stopping needs no confirmation: relay the summary. If the user mentions what
they're doing, pass it as `task`; it ends up on the invoice.

## Stop the timer yourself when the work you were timing is done

When the timer is tracking work **you** are carrying out — the user said
"start a timer and …", or asked you to time a task you then performed —
the timer is yours to stop. Don't wait to be asked, and don't leave it
running while you write up what you did:

1. Decide what "done" means from the request before you start — the
   acceptance criteria: the bug is fixed and the tests pass, the draft is
   written, the change is verified in the browser. Make sure the running
   timer is the one you started (`GET /timer` if unsure); never stop a
   timer the user started for their own work unless they say they're done.
2. Finish the work. If a step fails, keep the timer running while you fix
   it — the time was spent either way.
3. The moment the acceptance criteria are met, call `POST /timer/stop` with
   a one-line note of what was done and `"append": true`, so the entry's
   task reads "Nav rework — Done: rebuilt the nav, tests green". Do this
   **before** your final reply, then relay the stop summary in that reply:
   "Stopped Northwind / Redesign after 1h 40m — Nav rework — Done: …".
4. If you have to stop short — blocked on something only the user can
   decide — say so, say the timer is still running, and ask whether to stop
   it. Don't guess; a stopped timer under-bills and a forgotten one
   over-bills.

Finishing a task you were not timing is not a reason to stop a timer.

## Calls

### GET /timer
What is running. `{ running: entry | null, summary }`.

### GET /timer/projects
`{ projects: [{ id, name, clientName, label }], summary }`. `label` is
"Client / Project" and is what to show the user.

### GET /timer/report?range=month&groupBy=client&tz=<IANA zone>
Hours over a window, one row per `groupBy` (`client`, `project` — the
default — or `person`), each with billable and non-billable hours and how
much of the time was an assistant's: "Last month: 61h across 3 clients —
19h by an assistant, 42h by people. By client: Northwind 32h (assistant
11h, people 21h), $4,000; …" `range` is `today`, `week` (default),
`last-week`, `month`, `last-month`, `year` or `all`, or give `from`/`to`
dates; `billable=billable|nonBillable` narrows; `project` or `client`
narrows to one. `groupBy=person` lists each member and one row "Assistant".
Pass the user's zone as `tz` so the week starts where they are. Returns
`{ groups: [{ label, hours, billableHours, nonBillableHours, agentHours,
peopleHours, amountCents?, entries }], totals, window, excludedRunning,
summary }`. Money appears only for an owner or admin; a member sees their
own hours. A running timer is not counted — say so if `excludedRunning` is
above zero. Read-only: no confirmation needed.

### GET /timer/entries?project=<name>&range=month&by=agent&tz=<IANA zone>
The entries themselves, newest first, with hours split between an
assistant's entries and people's: "This month on Northwind / Redesign: 4h
21m in 3 entries — 2h 11m by an assistant, 2h 10m by people, $543.75 at
the project rates." Filter by `project` or `client` (named loosely, as for
start), a `range` (`today`, `week`, `last-week`, `month`, `last-month`,
`year`, `all`) or `from`/`to` dates, `by` (`agent`, `people`, `all`) and
`billed` (`false` for unbilled only). Without a project or client it covers
this week; with one, all time. Returns `{ entries: [{ date, project, task,
hours, byAgent, amountCents, invoiceNumber }], totals: { hours,
agentHours, peopleHours, amountCents }, summary }`. Same member scoping as
the report. Read-only: no confirmation needed.

### PATCH /timer/entries/:id
Change a completed entry. `:id` is an entry id from `GET /timer/entries`,
or `last` for the user's most recent entry. Body: any of `project` (move
it; id, name or "Client / Project"), `minutes` (a new duration keeping the
entry's end), `start` and `end` (ISO), `task` (`null` clears it),
`billable`, plus `"dryRun": true` to preview (`{ dryRun, changes, overlaps,
summary }`) — confirm the summary with the user, then call again without
it. The running timer is not an entry here (`entry_running`; stop it
first), another person's entry needs an owner or admin (`forbidden`), and
an entry already on an invoice is refused (`entry_invoiced`) — the message
says whether to take the line off the draft or void the invoice. The entry
keeps its source: an assistant's entry stays an assistant's.

### DELETE /timer/entries/:id?dryRun=true
Delete a completed entry (`:id` as above, `last` allowed). Destructive:
preview with `dryRun=true`, read the entry back, and delete only on a
clear yes. Same refusals as PATCH.

### GET /timer/budget?project=<name>&tz=<IANA zone>
Where a project stands against its hour budget or monthly retainer: hours
used, left, percent, days left in the period, where the current pace lands,
whether hours over the allowance would bill, open alerts, and the
assistant's share of the hours. Give `project`, or `client` for all of a
client's projects, or neither for every budgeted project. A project with
no budget says so. Fees and rates appear for owners and admins only.
Read-only.

### POST /timer/start
Body: `{ "project": "<id, name, or 'Client / Project'>", "task": "<what they're doing>", "dryRun": true|false }`.
`project` may be omitted to reuse the last project. With `dryRun`, returns
200 `{ dryRun: true, project: { id, label, clientName, name }, task, wouldStop, summary }`
and changes nothing. Without it, any running timer is stopped first and it
returns 201 with `{ entry, stopped, summary }`.

### POST /timer/stop
Body: `{ "task": "<optional note>", "append": true }`. With `append`, the note
is added to what was already there; without it, the note replaces it.
404 `no_running_timer` when nothing is running.

### POST /timer/log
Either `{ "project", "task", "minutes": 45 }` (ends now) or
`{ "project", "task", "start": "<ISO>", "end": "<ISO>" }`, plus `"dryRun": true`
to preview (`{ dryRun, project, task, start, end, overlaps, summary }`). Returns 201
when done for real. `overlaps` lists the user's entries (or running timer)
that share time with the new one; when it isn't empty the summary says so —
tell the user that time would count twice and log it only if they still want it.

## Errors

`{ error, message, details? }`. Notable:
- 401 — the token is wrong or revoked; ask the user for a new one.
- 402 `subscription_required` — the workspace's plan lapsed; tell the user to visit Settings → Plan & billing in Kairos.
- 403 `insufficient_scope` — the token only does timer things; don't try other routes.
- 422 `project_required` / `ambiguous_project` / `unknown_project` — `details.projects` lists options; `unknown_project` also carries `details.suggestions`, the nearest names.
- 422 `entry_running` / `entry_invoiced`, 403 `forbidden` — on an entry edit or delete; relay the message, it says what to do instead.

## Example

User: "Starting on the Northwind nav rework."
→ `POST /timer/start {"project":"Northwind","task":"Nav rework","dryRun":true}`
→ API: `{ project: { id: "…", label: "Northwind / Redesign" }, summary: "Would start Northwind / Redesign — Nav rework. Confirm to start." }`
→ ask: "Start Northwind / Redesign — Nav rework?"

User: "Yes."
→ `POST /timer/start {"project":"<id from the dry run>","task":"Nav rework"}`
→ relay: "Started Northwind / Redesign — Nav rework."

User, later: "Done for today."
→ `POST /timer/stop {}`
→ relay: "Stopped Northwind / Redesign after 1h 40m — Nav rework."

User: "Start a timer on Apex Medical and fix the login redirect bug."
→ dry run, confirm "Start Apex Medical / Service Contract — Login redirect bug?", start.
→ fix the bug, run the tests — done when they pass.
→ `POST /timer/stop {"task":"Done: redirect now keeps the return URL, tests green","append":true}`
→ relay: "Fixed it: the redirect now keeps the return URL and the tests pass. Stopped Apex Medical / Service Contract after 25m — Login redirect bug — Done: redirect now keeps the return URL, tests green."
