Files
agent-skills/skills/hourlog/SKILL.md
T
naps62 b453e72d42 Revert "feat: draw the grid by default, --markdown to opt out"
The drawn grid was solving the wrong problem: the script output was
already right, and fencing it in chat is what turned a rendered table
into raw dashes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 19:52:30 +00:00

139 lines
5.7 KiB
Markdown

---
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
`<skill-dir>/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 <skill-dir>/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 <skill-dir>/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 <skill-dir>/scripts/me-api.py confirm --entry ID --hours 8 --dry-run
# log a day with no allocation
python3 <skill-dir>/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.