--- name: hourlog description: Work out how many hours each client project got on each day, by reading Claude Code and Codex session activity, then reconcile that against the company timesheet and confirm the week's hours. Use when the user asks to log hours, fill in their timesheet, check what they worked on last week, or runs the Friday hour-logging session. user-invocable: true argument-hint: "[--week last|this | --since YYYY-MM-DD [--until YYYY-MM-DD]]" allowed-tools: - Read - Grep - Glob - Bash - Edit - Write --- # Hour log Turn session activity into an hours-per-project-per-day proposal, check it against the timesheet, and submit only what the user approves. The timesheet is a company record. Nothing is written to it without the user saying go, in this session, after seeing the table. An unattended run stops at the proposal. ## Setup (once per machine) Three things must exist. Check them before anything else and stop with the missing step if not. 1. `~/.config/hourlog/projects.json` — path prefix to project mapping. Copy `/config.example.json` and fill it in. **Never commit a filled config, and never put a project, client, or host name in this repo** — it is public. 2. `HOURLOG_API` and `HOURLOG_TOKEN` in `~/.env.claude`. The token is a personal access token from the timesheet app's profile page. Scopes: `profile:read`, `schedule:read`, and `schedule:write` only if submitting. Never echo the token. 3. Project names in the config must match the app exactly. Verify with `me-api.py projects` and fix the config, not the app. ## 1. Scan the sessions ```sh python3 /scripts/scan-activity.py --week last ``` Prints a markdown table: one row per day, one column per project holding `hours · share · active minutes`, then a totals row. That table is the deliverable — paste it as-is rather than restating it in prose. The unit behind it is a 5-minute slot in which at least one message was written, deduplicated per project — so a 40-subagent swarm on one project counts once, and two projects worked in parallel each keep their own slots. Message counts would let one overnight autonomous run outweigh a real morning. The `flags` column carries the two things that need judgement, not arithmetic: - **`outside HHh`.** Activity at 02:00 is usually an unattended run, not work. If most of a project's minutes came from 21h-02h, its share is inflated — say so and shift the split. - **`unmapped time excluded`.** A path with no rule, left out of the split, so that day is wrong rather than merely incomplete. Either it is a new client directory the config is missing — say so and ask — or it is personal work that belongs in `exclude`. Never guess it into a client project. `--json` carries the same fields if you need to compute against them. ## 2. Propose the hours **Active minutes are a floor on real work, never a measure of it.** Meetings, review, reading, whiteboarding, and thinking leave no transcript at all. A day showing 160 active minutes was not a two-hour day. Read the numbers as *proportions between projects*, and get the day's length from the calendar. The scan already does the arithmetic: it splits `nominal_day_hours` (config, default 8) across the day's mapped projects by share, in 15-minute steps. Your job is the judgement it cannot do: - One project at 85% or more: round it up to the whole day rather than leaving a token 1h on the other. - Days flagged `under 30min, treat as empty` get nothing proposed. - Correct the split for the two things flagged in step 1 before proposing it. Never scale a day *down* because its transcripts are thin. A quiet day is a normal working day unless the user says it was not, or the timesheet already marks it as time off or a holiday. ## 3. Reconcile against the timesheet ```sh python3 /scripts/me-api.py schedule --start YYYY-MM-DD --end YYYY-MM-DD ``` Each day comes back either already planned (an allocation with an entry id and planned hours) or absent. That splits the proposal in two: - **Planned and matching** — confirm at the planned hours. - **Planned but the sessions disagree** — confirm at the observed hours, and show both numbers in the table so the user sees what changed. - **Not planned at all** — needs a new entry, which is a bigger claim. Flag it separately rather than folding it in. ## 4. Show the table, then ask One row per project per day: date, weekday, project, proposed hours, what the timesheet says now, and the action (confirm / adjust / add / skip). Mark any day that needs a new entry rather than a confirmation. Keep it to that table plus a line for anything you had to judge — a discounted overnight run, an unmapped path, a day you left empty. No commentary on days that were straightforward. Then ask once, plainly, whether to submit. Wait for an answer. Silence, a timeout, or "user may be away" is not approval — leave the timesheet alone and say the run is waiting. ## 5. Submit what was approved ```sh # confirm a planned day python3 /scripts/me-api.py confirm --entry ID --hours 8 --dry-run # log a day with no allocation python3 /scripts/me-api.py log --project ID --dates D,D --hours 8 --dry-run ``` Run every write with `--dry-run` first and show the requests. Drop the flag only for the rows the user approved — not the whole table, if they approved part of it. A `423` means the period is locked and ops has to reopen it. Report it and move on; it is not a failure to retry. Re-read the schedule afterwards and confirm what landed. Report the diff, not an assumption. ## Scope How a day divided between projects, and confirming hours already worked. Not future allocations, not time off, not anyone else's schedule.