diff --git a/skills/hourlog/SKILL.md b/skills/hourlog/SKILL.md index 69a2ec7..715a5e8 100644 --- a/skills/hourlog/SKILL.md +++ b/skills/hourlog/SKILL.md @@ -43,20 +43,25 @@ missing step if not. python3 /scripts/scan-activity.py --week last ``` -Reports one line per project per day: suggested hours, share of the day, active -minutes, and the hour range they fell in. The unit 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. +Prints a table: one row per day, one column per project, hours in each cell. +That table is the deliverable — show it as-is rather than restating it. -Two things in the output need judgement, not arithmetic: +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. -- **`outside working hours` lines.** Activity at 02:00 is usually an unattended - run, not work. Discount it unless the user says otherwise. -- **`unmapped:` lines.** A path with no rule. 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. +Add `--json` for the evidence behind each cell: active minutes, share, hour +range, and which hours fell outside working hours. Read it before proposing +anything, and note two things there: + +- **Hours flagged `outside_workday`.** 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` entries.** A path with no rule, excluded from the split, so the + affected days are 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. ## 2. Propose the hours @@ -71,13 +76,8 @@ 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. -- Discount hours flagged `outside`. If most of a project's minutes came from - 21h-02h, its share is inflated by an unattended run — say so and shift the - split rather than passing the number through. -- Days marked `under 30min, treat as empty` get nothing proposed. -- An `unmapped:` line with real minutes means the split is wrong, not just - incomplete: those minutes were excluded from the division. Resolve the - mapping before proposing hours for that day. +- Days marked `*` in the table 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 @@ -100,9 +100,13 @@ planned hours) or absent. That splits the proposal in two: ## 4. Show the table, then ask -One row per project per day: date, weekday, project, proposed hours, what the -timesheet currently says, and the action (confirm / adjust / add / skip). -Flag every row where the evidence was weak or outside working hours. +The scan's table, one row per day and one column per project, with the +timesheet's current hours beside each proposed cell where the two differ. 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 diff --git a/skills/hourlog/scripts/scan-activity.py b/skills/hourlog/scripts/scan-activity.py index 6e5fdf2..d297349 100755 --- a/skills/hourlog/scripts/scan-activity.py +++ b/skills/hourlog/scripts/scan-activity.py @@ -242,29 +242,41 @@ def main(): print() return 0 - print(f"{since} .. {until} ({nfiles} sessions, {mins}-min slots, " - f"{cfg['timezone']}, {cfg['nominal_day_hours']}h day)") - print("hours are a suggested split of a full day, not measured time\n") - label_w = max([12] + [len(p["project"]) for e in report - for p in e["projects"]]) + cols = [p for p in dict.fromkeys( + x["project"] for e in report for x in e["projects"]) + if not p.startswith("unmapped:")] + widths = [max(len(c), 6) for c in cols] + head = "day " + " ".join( + f"{c:>{w}}" for c, w in zip(cols, widths)) + " total" + print(head) + print("-" * len(head)) + + totals = collections.Counter() + flagged = False for e in report: - if not e["projects"]: - continue - note = " — under 30min, treat as empty" if e["thin"] else "" - print(f"{e['date']} {e['weekday']} " - f"{e['total_active_minutes']}min active{note}") - for p in e["projects"]: - out = "" - if p["outside_workday"]: - out = " outside " + ",".join( - f"{h:02d}h" for h in p["outside_workday"]) - print(f" {p['project']:<{label_w}} " - f"{fmt_hours(p['suggested_hours']):>6} " - f"{p['share']*100:3.0f}% {p['active_minutes']:>4}min " - f"{p['first_hour']:02d}-{p['last_hour']:02d}h{out}") - print() + hours = {p["project"]: p["suggested_hours"] for p in e["projects"]} + day_sum = sum(hours.get(c, 0) for c in cols) + cells = " ".join( + f"{fmt_hours(hours[c]) if hours.get(c) else '—':>{w}}" + for c, w in zip(cols, widths)) + mark = " *" if e["thin"] and e["total_active_minutes"] else "" + flagged = flagged or bool(mark) + print(f"{e['date']} {e['weekday']} {cells} " + f"{fmt_hours(day_sum) if day_sum else '—':>5}{mark}") + for c in cols: + totals[c] += hours.get(c, 0) + + print("-" * len(head)) + print("total " + " ".join( + f"{fmt_hours(totals[c]):>{w}}" for c, w in zip(cols, widths)) + + f" {fmt_hours(sum(totals.values())):>5}") + + if flagged: + print("\n* under 30min of activity — treat as empty unless you know " + "otherwise") if unmapped: - print("unmapped paths — add them to the config or the exclude list:") + print("\nunmapped paths, excluded from the split — add them to the " + "config or the exclude list:") for u in unmapped: print(" ", u) return 0