From bf1dc761256b8e6fd582f1e01b22f6515ec1f58f Mon Sep 17 00:00:00 2001 From: naps62 Date: Fri, 14 Aug 2026 19:51:28 +0000 Subject: [PATCH] feat: draw the grid by default, --markdown to opt out A markdown table carries no borders of its own; whether any get drawn is up to the renderer, and terminal renderers mostly draw none. Emitting the grid directly makes the output look the same everywhere it lands. Co-Authored-By: Claude Opus 5 --- skills/hourlog/SKILL.md | 8 +++- skills/hourlog/scripts/scan-activity.py | 57 ++++++++++++++++++------- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/skills/hourlog/SKILL.md b/skills/hourlog/SKILL.md index e1b0f22..5b5ff4d 100644 --- a/skills/hourlog/SKILL.md +++ b/skills/hourlog/SKILL.md @@ -43,9 +43,13 @@ missing step if not. python3 /scripts/scan-activity.py --week last ``` -Prints a markdown table: one row per day, one column per project holding +Prints a drawn grid: 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. +deliverable — show it as-is, inside a fenced code block so the terminal +renderer leaves the borders alone, rather than restating it in prose. + +`--markdown` swaps the grid for pipes when the destination renders tables +itself (a PR body, an issue, a doc). 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 diff --git a/skills/hourlog/scripts/scan-activity.py b/skills/hourlog/scripts/scan-activity.py index 0925a0f..1c96b34 100755 --- a/skills/hourlog/scripts/scan-activity.py +++ b/skills/hourlog/scripts/scan-activity.py @@ -14,11 +14,16 @@ transcripts on one project counts once, and two projects worked in parallel each keep their own slots. Message counts would make one overnight autonomous run outweigh a real morning; wall-clock presence does not. -Output is one line per project per day: suggested hours, share of the day, and -the active minutes behind it. The suggestion splits a nominal working day by -share, in 15-minute steps — active minutes are a floor on real work, never a -measure of it, so the day's length comes from the calendar and only the split -between projects comes from the sessions. See SKILL.md. +Output is a table: one row per day, one column per project, each cell holding +suggested hours, share of the day, and the active minutes behind it. The +suggestion splits a nominal working day by share, in 15-minute steps — active +minutes are a floor on real work, never a measure of it, so the day's length +comes from the calendar and only the split between projects comes from the +sessions. See SKILL.md. + +Default rendering is a drawn grid, because a markdown table has no borders of +its own and terminal renderers vary in whether they draw any. --markdown emits +pipes instead, for pasting somewhere that does render them. """ import argparse import collections @@ -155,17 +160,35 @@ def fmt_hour_list(hours): return ", ".join(out) +def widths(rows): + return [max(len(r[i]) for r in rows) for i in range(len(rows[0]))] + + +def _cells(row, w, align_right): + return "| " + " | ".join( + c.rjust(w[i]) if i in align_right else c.ljust(w[i]) + for i, c in enumerate(row)) + " |" + + +def box_table(rows, align_right=()): + """Render rows (first is the header) as a grid with every cell bordered.""" + w = widths(rows) + rule = "+" + "+".join("-" * (n + 2) for n in w) + "+" + out = [rule] + for row in rows: + out += [_cells(row, w, align_right), rule] + return "\n".join(out) + + def md_table(rows, align_right=()): """Render rows (first is the header) as a padded markdown table.""" - w = [max(len(r[i]) for r in rows) for i in range(len(rows[0]))] - def line(cells): - return "| " + " | ".join( - c.rjust(w[i]) if i in align_right else c.ljust(w[i]) - for i, c in enumerate(cells)) + " |" + w = widths(rows) sep = "|" + "|".join( ("-" * (w[i] + 1) + ":") if i in align_right else ("-" * (w[i] + 2)) for i in range(len(w))) + "|" - return "\n".join([line(rows[0]), sep] + [line(r) for r in rows[1:]]) + return "\n".join( + [_cells(rows[0], w, align_right), sep] + + [_cells(r, w, align_right) for r in rows[1:]]) def main(): @@ -175,6 +198,8 @@ def main(): ap.add_argument("--week", choices=["last", "this"]) ap.add_argument("--config", default=CONFIG) ap.add_argument("--json", action="store_true") + ap.add_argument("--markdown", action="store_true", + help="pipe table for pasting, instead of a drawn grid") a = ap.parse_args() cfg = load_config(a.config) @@ -308,11 +333,13 @@ def main(): rows.append([f"{e['date']} {e['weekday']}"] + cells + [fmt_hours(day_sum) if day_sum else "—", "; ".join(flags)]) - rows.append(["**total**"] + - [f"**{fmt_hours(week[c])}**" for c in cols] + - [f"**{fmt_hours(sum(week.values()))}**", ""]) + bold = (lambda s: f"**{s}**") if a.markdown else (lambda s: s) + rows.append([bold("total")] + + [bold(fmt_hours(week[c])) for c in cols] + + [bold(fmt_hours(sum(week.values()))), ""]) right = set(range(1, len(cols) + 2)) - print(md_table(rows, align_right=right)) + render = md_table if a.markdown else box_table + print(render(rows, align_right=right)) if unmapped: print("\nunmapped paths — add them to the config or the exclude list:")