Work hours
Reconstructs working time after the fact from timestamps the person already produces. The answer is
only as good as two things: telling the person's own actions from automation, and deciding what a
silence means. Everything below is about getting those two right.
Read
references/methodology.md
before trusting any number — it lists the traps that each moved the answer
by hours.
has the per-source how-to.
Ground rules
- Personal data, temporary by default. Everything goes in the run dir from the config
(, default ). Never write extracted events into a repo. When the person
says they are done, delete the run dir and every upload (browser histories, chat exports, screen-time
databases); confirm the paths are gone.
- Read-only everywhere. Slack, GitHub, trackers, mail: search and read, never post or react.
- Throttle. Transcript folders run to gigabytes: , stream line by line,
one or two readers, .
- Ask for data, don't guess it. The person can give you things you cannot reach (chat exports, laptop
files, a chess username, their meeting schedule). Ask early; see "What to ask for".
Workflow
bash
S=<this skill>/scripts
# 0. config: copy references/config.example.json to ~/.config/work-hours/config.json and fill it in
# (timezone, window, customer rules, Slack user id, chess usernames, meeting schedule, categories).
# The person's real config never lives inside the skill.
python3 -I $S/extract_claude.py # human prompts + answers/interrupts from transcripts
# Slack: run the MCP search loop (references/sources.md), then:
python3 -I $S/slack_parse.py <saved result files...>
python3 -I $S/extract_whatsapp.py _chat.txt --me "<name>" --customer <c> --tag <who>
python3 -I $S/extract_chess.py # chess.com / lichess → leisure intervals
python3 -I $S/extract_meetings.py # recordings + recurring calls (run after the point extractors)
python3 -I $S/extract_mac.py <upload-dir> # laptop presence, foreground apps, browsing
# any other human-only source (PR approvals, tracker moves made in the UI): write events.<name>.ndjson
python3 -I $S/calibrate.py # silence survival, bout criterion, ground-truth check
python3 -I $S/model.py --idle 25 --tail 5 # → report.md
Point events are
(
,
,
,
); intervals are
intervals.<source>.ndjson
(
,
,
).
picks up every file matching those names,
so a new source is just a new file.
The model
Two independent methods. Report both; their agreement is the accuracy claim.
- B. Messages: one merged timeline of every human action across every source. A silence of up to
minutes is work; a longer one is a break that still credits minutes after the last action.
Leisure intervals (chess games, leisure browsing) are subtracted.
- A. Presence: laptop present (unlocked, display-sleep tail removed), minus leisure, plus off-laptop work
only where there is evidence: phone messages while the laptop was away, bridged when under
minutes apart, and calls. Do not let the message model's gap-filling run while the laptop says the person
left — that double-counts about 1.5 h/day.
Measure across all customers combined first; the per-customer split comes from attributing each worked
minute to the nearest action's customer. Cut workdays at a fixed local hour (default 05:00), so a 02:00
session belongs to the day before.
Choosing the idle threshold (the number that matters most)
On one real dataset the answer swung from 6.0 to 13.9 h/day between a 5- and 60-minute threshold, and
the gap histogram had no natural break. Calibrate it; don't pick it.
- gives the bout criterion: a lower bound, usually around 10 minutes.
- Ground truth from the person's own words. Read their messages for "lunch", "brb", "back",
"going to sleep", "on the train, working from my phone", in every language they use. Read each hit
yourself: a word list must not decide meaning. Write with and
stretches. The right threshold separates the two. In the calibration run, stated breaks always left a
silence of 27 minutes or more and phone work never exceeded about 26, so the answer was 25 minutes.
- Attention check. In agent-driven work, see whether the person replies seconds after an agent finishes
(they were watching) or long after (they were away). If replies come about 16 minutes after the agent
finished and almost never within 2 minutes, a silence is not attentive waiting, so do not stretch the
threshold to cover it.
What to ask for
Ranked by value:
- Laptop activity. Send
references/mac-upload-prompt.md
to an agent on their Mac. It uploads browser
histories, (screen on, foreground app) and the lock/power logs. This is the strongest
signal.
- Chat exports with the people they work with (WhatsApp "Export chat").
- Usernames for leisure that has an API (chess.com, lichess).
- Recurring meetings: day, time, length, and when they changed.
- Slow exports, if they want more precision: Google Takeout (YouTube and Maps), Apple Health (sleep).
iPhone Screen Time only reaches the Mac if "Share Across Devices" is on, and even then it sits in system
stores that need Full Disk Access.
- Vacations and other periods to exclude, as config .
Reporting
Lead with the combined hours per weekday and weekend day, then the range across methods and thresholds.
Then cover the per-customer split, the clock-time routine (start, meal dips, second shift, end) and how much
leisure was found inside "work". Name what is still invisible: phone screen time, unrecorded calls,
thinking away from the keyboard.