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 <noreply@anthropic.com>
This commit is contained in:
naps62
2026-08-14 19:51:28 +00:00
parent ea9c3acecf
commit bf1dc76125
2 changed files with 48 additions and 17 deletions
+6 -2
View File
@@ -43,9 +43,13 @@ missing step if not.
python3 <skill-dir>/scripts/scan-activity.py --week last python3 <skill-dir>/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 `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 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 written, deduplicated per project — so a 40-subagent swarm on one project
+42 -15
View File
@@ -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 each keep their own slots. Message counts would make one overnight autonomous
run outweigh a real morning; wall-clock presence does not. 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 Output is a table: one row per day, one column per project, each cell holding
the active minutes behind it. The suggestion splits a nominal working day by suggested hours, share of the day, and the active minutes behind it. The
share, in 15-minute steps — active minutes are a floor on real work, never a suggestion splits a nominal working day by share, in 15-minute steps — active
measure of it, so the day's length comes from the calendar and only the split minutes are a floor on real work, never a measure of it, so the day's length
between projects comes from the sessions. See SKILL.md. 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 argparse
import collections import collections
@@ -155,17 +160,35 @@ def fmt_hour_list(hours):
return ", ".join(out) 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=()): def md_table(rows, align_right=()):
"""Render rows (first is the header) as a padded markdown table.""" """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]))] w = widths(rows)
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)) + " |"
sep = "|" + "|".join( sep = "|" + "|".join(
("-" * (w[i] + 1) + ":") if i in align_right else ("-" * (w[i] + 2)) ("-" * (w[i] + 1) + ":") if i in align_right else ("-" * (w[i] + 2))
for i in range(len(w))) + "|" 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(): def main():
@@ -175,6 +198,8 @@ def main():
ap.add_argument("--week", choices=["last", "this"]) ap.add_argument("--week", choices=["last", "this"])
ap.add_argument("--config", default=CONFIG) ap.add_argument("--config", default=CONFIG)
ap.add_argument("--json", action="store_true") 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() a = ap.parse_args()
cfg = load_config(a.config) cfg = load_config(a.config)
@@ -308,11 +333,13 @@ def main():
rows.append([f"{e['date']} {e['weekday']}"] + cells + rows.append([f"{e['date']} {e['weekday']}"] + cells +
[fmt_hours(day_sum) if day_sum else "", "; ".join(flags)]) [fmt_hours(day_sum) if day_sum else "", "; ".join(flags)])
rows.append(["**total**"] + bold = (lambda s: f"**{s}**") if a.markdown else (lambda s: s)
[f"**{fmt_hours(week[c])}**" for c in cols] + rows.append([bold("total")] +
[f"**{fmt_hours(sum(week.values()))}**", ""]) [bold(fmt_hours(week[c])) for c in cols] +
[bold(fmt_hours(sum(week.values()))), ""])
right = set(range(1, len(cols) + 2)) 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: if unmapped:
print("\nunmapped paths — add them to the config or the exclude list:") print("\nunmapped paths — add them to the config or the exclude list:")