feat: add the week-review skill

Codifies the session-review process: read open issues for carry-over,
scan transcripts, rank by how often the same correction repeated, check
the current model docs before recommending, then apply or file.

scripts/scan-sessions.py enumerates top-level sessions in a window,
drops subagent transcripts, and flags swarm runs so a single 577-session
security scan does not read as 40% of the week's work.
This commit is contained in:
naps62
2026-08-01 15:15:14 +00:00
parent ad4d14e078
commit f6b58e8849
3 changed files with 278 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
---
name: week-review
description: Review the past week of Claude Code and Codex sessions, find recurring friction, and turn it into concrete config or tooling changes. Use when the user asks to review the week, review recent sessions, or asks what to improve about their setup. Also picks up carry-over items filed as issues on the agent-skills repo.
user-invocable: true
argument-hint: "[--days N | --since YYYY-MM-DD]"
allowed-tools:
- Read
- Grep
- Glob
- Bash
- Edit
- Write
- WebFetch
- WebSearch
---
# Week review
Find what went wrong repeatedly, fix the cause, file the rest.
The output is changes and issues, not a report. A finding nobody acts on was
not worth the tokens to produce.
## 1. Carry-over first
Read the open issues before scanning anything. Last week's unfinished work is
the highest-value input, and re-deriving it from transcripts wastes a lot of
context.
```sh
source ~/.env.claude
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.naps.pt/api/v1/repos/yolo/agent-skills/issues?state=open&limit=50" \
| python3 -c "import json,sys; [print(f\"#{i['number']} {i['title']}\") for i in json.load(sys.stdin)]"
```
Ask which of them to take this week. Do not silently re-litigate one the user
already deferred — a deferred item stays open and is mentioned in one line.
## 2. Scan
```sh
python3 <skill-dir>/scripts/scan-sessions.py --days 7 --out <scratch>
```
Writes `sessions.json` (one record per session) and `userturns.txt` (every
human turn, grouped). It drops subagent transcripts and flags swarm runs —
collapse those to a single line, since one `/code-review ultra` can be 500+
sessions and 40% of the week's bytes without being 40% of the week's work.
Read `userturns.txt` in full. It is the primary evidence and it is usually
40-100k tokens. Do not sample it.
## 3. Find the friction
Rank by how often the same thing went wrong, not by how annoying any one
instance felt. In order of signal strength:
- **The same correction given more than once**, especially across different
repos. Four separate "stop putting decisions in the spec, use an ADR"
corrections means the rule belongs in global config, not in each repo.
- **A fix that did not hold.** Something declared fixed in one session and
recurring days later. Name both sessions.
- **Crashes, `/compact` confusion, "are you there?", sessions restarted to
rebuild lost state.**
- **Security slips** — a secret echoed, an env value written somewhere it
persists. These outrank everything above on consequence.
- **Anything the user said twice in different words.**
Quote the user verbatim with the date and repo. A finding without a quote is a
guess, and the user can tell.
## 4. Check the docs before recommending
Model behaviour changes and last year's advice rots. Before proposing a
prompt, skill, or config change, read the relevant page — do not answer from
memory:
- `platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5`
- `.../prompting-claude-fable-5`
- `.../claude-prompting-best-practices`
- `code.claude.com/docs/en/memory`
Two findings from these that keep mattering: instructions to verify or
re-check compound badly and should be removed, and prompt style leaks into
output style, so a rule written in dense prose teaches dense prose.
Search for community practice too, and say which source a recommendation came
from.
## 5. Measure before trimming
Always-loaded and on-demand are different budgets, and conflating them
produces wrong advice.
| Always loaded | On demand |
|---|---|
| entry files and every `@import` they pull | skill bodies |
| `~/.claude/rules/*.md` without `paths:` frontmatter | `~/.claude/rules/*.md` **with** `paths:` |
| the first 200 lines of each project's `MEMORY.md` | memory topic files |
| every skill's `name` + `description` | |
Count lines, not words — Anthropic's target is under 200 lines per file.
Splitting one file into `@import`s saves nothing; only deleting content or
adding `paths:` scoping does.
## 6. Apply, then file the rest
Propose a ranked shortlist with an appetite for each. Apply what the user
agrees to, in this repo, and push. For anything deferred or too large, file a
Gitea issue so next week starts from step 1 instead of a re-derivation.
**This repo is public.** Issues must carry no client names, no hostnames, no
secrets, no internal ticket IDs. Describe the shape of the problem, not the
customer it happened at. When quoting the user as evidence, strip identifying
detail first.
```sh
source ~/.env.claude
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"https://git.naps.pt/api/v1/repos/yolo/agent-skills/issues" \
-d '{"title":"...","body":"..."}'
```
Close issues that got done this week, with a one-line comment saying what
landed.
## Scope
Config, skills, hooks, and prompts. Not a project status report — the user has
trackers for that. If a week's biggest problem is a product bug, say so in one
line and move on.
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Enumerate Claude Code sessions in a window and dump their human turns.
Usage: scan-sessions.py [--days N] [--since YYYY-MM-DD] [--out DIR]
Writes two files to --out (default: cwd):
sessions.json one record per top-level session, oldest first
userturns.txt every human turn, grouped by session, for reading
A "top-level" session is one with no isSidechain marker and at least one real
human turn, which drops subagent transcripts. Swarm runs still show up as many
sessions sharing one cwd and timestamp — the report should collapse those.
"""
import argparse
import datetime as dt
import glob
import json
import os
import sys
SKIP_PREFIXES = (
"<local-command", "<command-", "<task-notification", "<system-reminder",
"Caveat:", "Base directory for this skill:",
)
def human_turns(path):
"""Yield (timestamp, text) for each real human turn in a transcript."""
for line in open(path, errors="replace"):
try:
d = json.loads(line)
except ValueError:
continue
if d.get("isSidechain") or d.get("type") != "user":
continue
c = (d.get("message") or {}).get("content")
if isinstance(c, list):
c = " ".join(
x.get("text", "") for x in c
if isinstance(x, dict) and x.get("type") == "text"
)
if not isinstance(c, str):
continue
c = " ".join(c.split())
if not c or c.startswith(SKIP_PREFIXES):
continue
if "This session is being continued" in c[:60]:
continue
yield d.get("timestamp"), c
def scan(root, cutoff):
out = []
for f in glob.glob(os.path.join(root, "*", "*.jsonl")):
try:
st = os.stat(f)
except OSError:
continue
if st.st_mtime < cutoff:
continue
turns, first_ts, sidechain, nlines = [], None, False, 0
try:
for line in open(f, errors="replace"):
nlines += 1
try:
d = json.loads(line)
except ValueError:
continue
if d.get("isSidechain"):
sidechain = True
break
if first_ts is None and d.get("timestamp"):
first_ts = d["timestamp"]
if d.get("cwd") and "cwd" not in locals():
pass
if sidechain:
continue
turns = list(human_turns(f))
except OSError:
continue
if not turns:
continue
cwd = None
for line in open(f, errors="replace"):
try:
cwd = json.loads(line).get("cwd")
except ValueError:
continue
if cwd:
break
out.append({
"ts": first_ts or "",
"cwd": cwd or os.path.basename(os.path.dirname(f)),
"mb": round(st.st_size / 1048576, 1),
"lines": nlines,
"turns": [t for _, t in turns],
"file": f,
})
out.sort(key=lambda x: x["ts"])
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--days", type=int, default=7)
ap.add_argument("--since")
ap.add_argument("--out", default=".")
ap.add_argument("--root", default=os.path.expanduser("~/.claude/projects"))
a = ap.parse_args()
if a.since:
cutoff = dt.datetime.fromisoformat(a.since).timestamp()
else:
cutoff = (dt.datetime.now() - dt.timedelta(days=a.days)).timestamp()
sessions = scan(a.root, cutoff)
os.makedirs(a.out, exist_ok=True)
with open(os.path.join(a.out, "sessions.json"), "w") as fh:
json.dump([{k: v for k, v in s.items() if k != "turns"} for s in sessions], fh, indent=1)
with open(os.path.join(a.out, "userturns.txt"), "w") as fh:
for s in sessions:
fh.write(f"\n===== {s['ts'][:16]} {s['cwd']} ({s['mb']}MB) =====\n")
for t in s["turns"]:
fh.write("- " + t[:500] + "\n")
by_cwd = {}
for s in sessions:
by_cwd[s["cwd"]] = by_cwd.get(s["cwd"], 0) + 1
swarms = {k: v for k, v in by_cwd.items() if v > 20}
print(f"{len(sessions)} top-level sessions, "
f"{sum(len(s['turns']) for s in sessions)} human turns, "
f"{sum(s['mb'] for s in sessions):.0f}MB")
if swarms:
print("likely swarm runs (collapse these to one line in the report):")
for k, v in sorted(swarms.items(), key=lambda x: -x[1]):
print(f" {v:4} sessions {k}")
print(f"wrote {a.out}/sessions.json and {a.out}/userturns.txt")
if __name__ == "__main__":
sys.exit(main())