--- name: time-tracking description: > Live time tracking for working several projects in parallel, backed by the `efforts` CLI. Use this skill when the user says they started or stopped working on something, wants to clock in or clock out, asks what they are working on right now, asks to log time or track time against a project, asks how long they spent on a task, or wants today's or this week's hours. Also use it for the analysis side: how much did today actually cost per task, what would today look like if the work had been done sequentially, how parallel was my week, how much of my time was overlap, where did my time really go, how many times did I switch context. Handles concurrent overlapping entries as the normal case. --- # Time tracking A live clock for parallel work. Every operation goes through the tracker CLI, which owns the files and the arithmetic: ``` efforts --json ``` If the shell answers `command not found`, the CLI is not installed: `npm i -g efforts`. Commands below are written bare (`start`, `stop`, `report`); prefix each with `efforts`. **Never read or write files under the data directory directly.** Duration maths, overlap attribution and the day-file format are the CLI's job. Hand-editing JSON is how totals go quietly wrong. ## Scope This skill drives a **live clock**: it starts and stops entries as work happens, and reports on entries that have already been tracked. It does not reconstruct a past week from calendars, chat history or meeting notes — it only knows what was tracked here. ## Phrase → command | The user says | Run | | :-- | :-- | | "I started working on X" / "clock me in on X" | `start --project

` | | "I started X at 9" / "…20 minutes ago" | `start … --at 9:00` / `--at -20m` | | "also starting Y" / "I'm working on Y too" | `start` again — do **not** stop anything | | "I stopped working on X" / "clock out of X" | `stop ` | | "I'm done for now" (one thing running) | `stop` | | "I did X from 9 to 10" / "I had a meeting 11 to 12" | `log --project

--from 9:00 --to 10:00` | | "I finished X, now doing Y" (same clock, new activity) | `split --at --first-task X --task Y` | | "I'm now working on Y instead" | `switch --project

` | | "what am I working on" / "what's running" | `status` | | "today's hours" / "today's work sheet" | `today` | | "this week's hours" | `report --week` | | "how long did I spend on X" | `report --week --project

` | | "how much did today actually cost per task" | `today --attribute` | | "what would today look like sequentially" | `today --attribute` | | "how parallel was today" / "how much was overlap" | `analyze` | | "give me the data" / "export it" | `export --from D --to D --format csv` | | "note that …" | `note --last ` | | "scratch that note" / "take that note off" | `note --rm` | | "that's ticket ENG-412" | `link ticket=ENG-412` | | "actually that started at 8" | `edit --start 8:00` | | "delete that entry" | `rm ` | | "back on what I was doing before" | `resume [query]` | Everything comes back as one JSON envelope: ``` success → exit 0, {"ok": true, "schemaVersion": 3, "command": …, "data": {…}, "warnings": […]} failure → exit 1, {"ok": false, "schemaVersion": 3, "command": …, "error": …, "hint": …} ``` There is no rendered output to relay — `data` is the whole contract, and building the view is your job. See **Rendering**. On `ok: false`, tell the user the `error` and act on the `hint` — don't retry blindly. ## Rules that matter **Concurrency is normal.** Several entries open at once is the expected state, not a mistake. `start` never stops anything. When `start` reports `data.alsoOpen`, mention what else is running as information — it is not a warning. **Never guess which entry to stop.** If "I stopped" arrives with more than one entry open, the CLI returns an error listing the candidates. Run `status`, show them, and **ask which one**. Do not pick the oldest, the newest, or the one that seems most likely. **`switch` is destructive.** It closes *everything* currently open before starting the new entry. Report every entry it closed, with durations, from `data.closed`. **Never hand-build a finished entry or a boundary.** For work that is already over, `log` writes a closed entry in one command — `start --at` followed by `stop --at` leaves the entry genuinely open in between, and if anything else is running that `stop` goes ambiguous and refuses. For an activity that changed mid-clock, `split` cuts one entry in two at a single instant. Retyping a boundary as `HH:MM` against a stored second-precision timestamp silently manufactures a gap or an overlap; both commands take the boundary once and apply it to both sides. **`log --to` may run past the current time.** A block stated as a whole — "grooming from 10:30 to 12pm", said at 11:58 — goes in as one `log`, ending at 12:00. Do not clip the end back to now and do not leave the entry open instead: both write a time the user never said. `start --at` and `stop --at` still refuse a future instant, and a bare future `HH:MM` there means the CLI read yesterday's time as today's. **One task per activity.** Task names describe the activity, not the ticket — `Account refresh — manual verification`, not the ticket title with the activity in a note. Reports group on the exact task string, so an activity buried in a note can never be broken out afterwards. The `ticket=` link is what ties a ticket's several task lines together. **A note is one attribute, not a log.** Each entry carries a single `note` string. Writing a second one **replaces** the first — the response echoes the replaced text, so relay it rather than letting it disappear. Notes describe the work in that entry's window; anything about *future* or deferred work does not belong on a past entry, because it will read as tracked effort later. **A project is required.** If the user didn't name one, check `status` and `today` for context and the config's `defaultProject`. Ask only when it is genuinely unclear. **Relay warnings verbatim.** In particular `matched project "…" by similarity` — a fuzzy project match that may have merged two real projects. The user needs to see this to catch it. ## Explaining attribution On a parallel day the raw durations add up to more hours than the day contains. Attribution splits each *moment* among the tasks running at that moment, so per-task totals add up to the wall clock the work actually occupied. When you show attributed numbers, **always show raw beside attributed and say which strategy was used in plain language** — the `data.attribution.explanation` field carries the wording. A single unexplained number that disagrees with the user's own sense of their day reads as a bug. Strategies (`--strategy`): - `equal` (default) — time split evenly among tasks running at the same moment - `weighted` — split in proportion to per-entry `--weight`, for one foreground task among background ones - `exclusive` — each moment goes entirely to the most recently started open task, modelling "only one thing really had my attention" Be careful about what this claims. It apportions elapsed time, which is arithmetic. It does **not** say how long a task would have taken without multitasking overhead — timestamps cannot answer that. If the user asks the counterfactual, say so, and offer `analyze`: the gap between raw and attributed (`overlapMinutes`) is how much apparent effort was overlap. ## Rendering **The CLI renders nothing. You build the view.** `today` / `report` return `data` grouped project → task → ticket, with per-project subtotals and a range total; `analyze` returns the overlap summary. There is no `message` field to pass through — it was removed precisely because a display baked into the CLI is a guess about a surface it cannot see. **In this terminal, never emit a markdown pipe table.** It renders as literal `|` characters. Use a fenced code block with fixed-width columns, padded so they line up in a monospace font. Elsewhere — a doc, a Linear comment, a web view — render whatever that surface actually supports. Whatever the form, carry these across from `data`: - **`entryIds` for every row**, comma-separated where a row groups several entries. `note`, `link`, `edit` and `rm` all take an id; a row that cannot name its entries leaves the user unable to act on what they are reading. - **`ticket` per row, and rows split by it** — the same activity on two tickets is two rows with separate totals, which is the point. Don't merge them back. If the user asks for one number per activity, sum them and say you did. - **The ticket's title beside its key**, resolved at render time — see **Ticket titles**. - **Still-running entries flagged**, with elapsed time (`open: true`, and `openCount` on the range). - **Raw beside attributed** whenever `--attribute` is on, never attributed alone. From `analyze`, the numbers worth saying out loud: overlap factor, minutes at each concurrency level from `concurrencyHistogram`, and `contextSwitches`. ### Ticket titles **A bare key is not what the user recognises.** `ACME-374` says nothing about which piece of work a row is; the title does. Whenever a view has rows carrying a `ticket` link, resolve those keys to titles and show both: ``` Task Ticket Title Raw Entry ids Initial implementation ACME-374 Push notification permission flow 3h09m x9d0rs, uxd1zy ``` Where a title would blow the column out, truncate it — the key stays exact, the title is context. On single-entry receipts (`start`, `stop`, `switch`, `resume`) put it inline instead of in a column. **The title is display only — never store it.** `data` carries the key and nothing else, by design: titles get renamed in the tracker, and a copy frozen into a task name or a note would quietly disagree with the tracker forever. It also collides with **One task per activity** — the task string names the activity, so a title pasted there destroys the grouping. Resolve on every render instead. **One lookup per distinct key, not per row or per entry.** A week's report can hold dozens of rows across a handful of tickets; resolve the set of keys once and reuse the titles across every row that shares one. When the tools support fetching several issues at a time, prefer that over a call each. **Never delay the clock for a lookup.** `start`, `stop` and `switch` run first and get reported first; a title that arrives after is an improvement to the receipt, not a precondition for it. **Degrade to the key, silently and always.** No tracker server, an unauthorized one, an unknown key, a key from a tracker you can't reach — show the key alone and carry on. Say the title couldn't be resolved once, not once per row, and **never invent a plausible title**: a guessed one is worse than none, because it reads as fact. A key whose prefix belongs to no configured tracker is a normal outcome here, not an error worth stopping over. ## Linking to trackers Entries carry arbitrary `links`, so the CLI stays service-agnostic. For a Linear issue, start the entry with a link and resolve the title for the receipt (see **Ticket titles**): ``` mcp__claude_ai_Linear__* # issue-reading tools; see the auth note below efforts start "Fix checkout webhook retry" --project client-co --link ticket=ENG-412 --json ``` **The key is always `ticket`, whatever the tracker is.** Not `linear=`, not `jira=`, not `github=` — one key, so a report can filter on it and a ticket's history stays in one place. The key space is free-form and nothing validates it, so a second spelling silently forks that history into two, and there is no repair command: fixing it means `edit --link` on every affected entry, found by hand. Reserve other keys for genuinely different things (`pr=`, `doc=`) on an entry that may also carry a `ticket=`. **Don't assume a server name — look at the tools actually available.** Linear MCP servers are installed per machine and namespaced differently depending on how they were added (`mcp__claude_ai_Linear__*` and `mcp__linear-server__*` are both real in the wild). Search for the Linear tools rather than calling a hardcoded name. **Auth may be a prerequisite.** If the only Linear tools present are `authenticate` and `complete_authentication`, the server is installed but unauthorized: the real issue-reading tools do not exist yet and appear only after OAuth. Call `authenticate`, give the user the authorization URL, then pass the callback URL from their browser to `complete_authentication`. Some setups instead use a one-time `/mcp` OAuth with no auth tool at all — again, go by which tools are present. If no Linear server is available, or the user would rather not authorize right now, **degrade rather than fail**: record `--link ticket=ENG-412` with the key alone and say the title couldn't be resolved. Never block starting an entry on an integration — the clock matters more than the title. Reports go the other way: `report --week --attribute --format json` is the documented contract for anything downstream, including timesheet generation. See `docs/SCHEMA.md`. ## Setup problems A config or data-directory error carries its own `hint` — act on that rather than guessing at paths. `efforts config` reports the settings it resolved and which file supplied each one, which is the question behind "why is it writing there" or "why is the timezone wrong". The ledger directory is created on the first write, so "does not exist yet" on a new machine is normal.