diff --git a/.claude/skills/xce-changes/SKILL.md b/.claude/skills/xce-changes/SKILL.md new file mode 100644 index 00000000..f12c2b48 --- /dev/null +++ b/.claude/skills/xce-changes/SKILL.md @@ -0,0 +1,282 @@ +--- +name: xce-changes +description: Generate or update a component-grouped change log for XCE from the last week's commits, written to CHANGES.md. Use when the user asks for a changelog, change log, "what changed this week", or to update CHANGES.md. +allowed-tools: Read, Write, Edit, Bash, Glob, Grep +argument-hint: [--week N] [--since YYYY-MM-DD --until YYYY-MM-DD] [--component ] [--output ] +triggers: + - /xce-changes + - update changelog + - update changes.md + - generate change log + - component changelog +--- + +# xce-changes Skill + +Generate/update `CHANGES.md` at the repo root: a rolling, component-grouped +change log. Each run captures one week of commits and writes (or refreshes) +that week's section, organized into subsections per XCE component +(`controller`, `worker`, `scheduler`, `autoscaler`, `frontend`, `xceadmin`, +`lib`, `include`, `schema`, `docs`, `specs`, `build`, `misc`). Older weeks are +preserved below, newest week on top — this is different from `/weekly_report` +(narrative status report, dated file per week); this is a mechanical, +per-component change ledger. + +## Invocation + +``` +/xce-changes # last complete week -> CHANGES.md +/xce-changes --week 2 # two weeks back +/xce-changes --since 2026-08-20 --until 2026-08-27 +/xce-changes --component scheduler # regenerate only the scheduler subsection for the window +/xce-changes --output docs/CHANGES.md # non-default location +``` + +### Arguments + +| Arg | Default | Meaning | +|-----|---------|---------| +| `--week N` | `1` | Weeks back. `1` = last complete week (Mon–Sun), `0` = current partial week. | +| `--since` / `--until` | *(unset)* | Explicit `YYYY-MM-DD` range, overrides `--week`. Both required together. | +| `--component ` | *(all)* | Only (re)build the named component's subsection for the resolved window; leave every other component's existing entry in that section untouched. | +| `--output ` | `CHANGES.md` (repo root) | File to write/update. Created if missing. | + +## Component mapping + +Map each changed file's path to exactly one component bucket — do not reuse +the generic `awk -F/ '{print $1}'` bucketing from `/weekly_report`, since +`src/` here mixes real components with build/config directories that would +otherwise get miscategorized as fake "components": + +| Path prefix | Component | +|---|---| +| `src/lib/` | `lib` | +| `src/controller/` | `controller` | +| `src/worker/` | `worker` | +| `src/scheduler/` | `scheduler` | +| `src/autoscaler/` | `autoscaler` | +| `src/frontend/` | `frontend` | +| `src/xceadmin/` | `xceadmin` | +| `src/include/` | `include` | +| `src/schema/` | `schema` | +| `src/docs/`, `docs/` | `docs` | +| `.kiro/` | `specs` | +| `src/meson.build`, `src/flake.*`, `src/tools/`, `src/configs/`, `src/nixSupport/`, `src/xce.build*`, `src/build*` | `build` | +| `.wolf/` | **exclude entirely** — bookkeeping, never appears in the changelog | +| anything else (root files, `scripts/`, `reports/`, `.claude/`) | `misc` | + +A commit can land in multiple components if it touched multiple areas — +list it under every component subsection it touches, not just one. + +## Procedure + +### Step 1 — Resolve the window + +Same reasoning as `/weekly_report`: use `python3` for date math, never +`date -d` with a relative expression (BusyBox `date` silently no-ops on +relative arithmetic and yields empty vars — see `.wolf/cerebrum.md`'s +bug-1628 entry). + +```bash +WEEK_OFFSET=1 # from --week; ignored if --since/--until given + +eval "$(python3 - "$WEEK_OFFSET" <<'PY' +import sys, datetime +off = int(sys.argv[1]) +t = datetime.date.today() +start = t - datetime.timedelta(days=t.weekday()) - datetime.timedelta(weeks=off) +end = start + datetime.timedelta(days=6) +f = lambda d: d.strftime('%b %d, %Y') +print(f'START={start}\nEND={end}\nHEADER={f(start)} - {f(end)}') +PY +)" +echo "window: $START .. $END" +``` + +If `--since`/`--until` were passed, set `START`/`END` from them directly and +compute `HEADER` the same way. Sanity-check both are non-empty before +continuing. + +### Step 2 — Continuity check against the existing file + +```bash +OUT="${OUTPUT:-CHANGES.md}" +grep -n '^## ' "$OUT" 2>/dev/null | head -5 +``` + +- If `$OUT` doesn't exist yet, this is a first run — skip to Step 3 and + create it fresh. +- If a section header for this **exact** window already exists (matched by + its `` marker comment, not just the printed + date range), this is an **update**: rebuild that section's content from + git truth and replace it in place — do not append a duplicate. Announce + this to the user ("refreshing existing Aug 24–30 section"). + - If `--component` was passed, only replace that one subsection inside the + existing window section, leaving sibling subsections as they are. +- If the most recent existing window's end date is `>= $START`, there's an + overlap — narrow `START` to `last_end + 1 day` so the same commit isn't + logged twice under two different headers. Tell the user about the + adjustment. +- Otherwise this is a new section — it will be **prepended** (newest window + first, right after the `# XCE Change Log` title line). + +### Step 3 — Gather commits and bucket by component + +```bash +git log --all --no-merges --since="$START 00:00:00" --until="$END 23:59:59" \ + --date=format:'%Y-%m-%d' --pretty=format:'@@%H%x09%h%x09%ad%x09%s' --name-only \ + -- . ':(exclude).wolf/*' +``` + +Feed that output to a small python3 pass (parsing name-only git log by hand +in bash is error-prone with filenames containing spaces — don't): + +```python3 +import re, subprocess, sys + +def bucket(path): + rules = [ + ('src/lib/', 'lib'), ('src/controller/', 'controller'), + ('src/worker/', 'worker'), ('src/scheduler/', 'scheduler'), + ('src/autoscaler/', 'autoscaler'), ('src/frontend/', 'frontend'), + ('src/xceadmin/', 'xceadmin'), ('src/include/', 'include'), + ('src/schema/', 'schema'), ('src/docs/', 'docs'), ('docs/', 'docs'), + ('.kiro/', 'specs'), + ] + for prefix, name in rules: + if path.startswith(prefix): + return name + if path.startswith('.wolf/'): + return None + build_prefixes = ('src/meson.build', 'src/flake.', 'src/tools/', + 'src/configs/', 'src/nixSupport/', 'src/xce.build', + 'src/build') + if path.startswith(build_prefixes): + return 'build' + if path.startswith('src/'): + return 'build' + return 'misc' + +BOOKKEEPING_SUBJECTS = re.compile( + r'^(chore\(wolf\)|save wolf|commit wolf|docs: sync OpenWolf state)') + +commits = {} # sha -> {sha7, date, subject, components:set} +order = [] +cur = None +for line in sys.stdin: + line = line.rstrip('\n') + if line.startswith('@@'): + sha, sha7, date, subject = line[2:].split('\t', 3) + if BOOKKEEPING_SUBJECTS.match(subject): + cur = None + continue + cur = sha + commits[sha] = {'sha7': sha7, 'date': date, 'subject': subject, + 'components': set()} + order.append(sha) + elif line.strip() and cur: + c = bucket(line) + if c: + commits[cur]['components'].add(c) + +by_component = {} +for sha in order: + c = commits[sha] + if not c['components']: + continue + for comp in c['components']: + by_component.setdefault(comp, []).append(c) + +COMPONENT_ORDER = ['controller', 'worker', 'scheduler', 'autoscaler', + 'frontend', 'xceadmin', 'lib', 'include', 'schema', + 'docs', 'specs', 'build', 'misc'] +for comp in COMPONENT_ORDER: + if comp not in by_component: + continue + print(f'### {comp}') + for c in by_component[comp]: + print(f"- `{c['sha7']}` {c['subject']} ({c['date']})") + print() +``` + +Run the `git log` piped directly into this script (heredoc or a temp `.py` +file — either is fine, this is a one-shot generation step, not a checked-in +script). + +If `--component ` was passed, only emit that one component's block and +splice it into the existing section rather than regenerating all of them. + +### Step 4 — Compose the section + +```markdown +##
+ + +### controller +- `` () + +### scheduler +- `` () + +... +``` + +- Keep the raw conventional-commit subject line verbatim (it already encodes + type/scope, e.g. `fix(bug-1561): ...`) — don't reword or summarize it. +- Omit component subsections with zero commits in this window; don't print + empty headers. +- If the window has **no** non-bookkeeping commits at all across every + component, write a single line under the header: `_No changes recorded._` + — don't fabricate content, and don't silently skip writing the section. +- For `fix(bug-N)` subjects, optionally enrich with a 6–10 word gist from + `.wolf/buglog.json`'s matching entry's `error_message` if it adds real + information beyond the subject line — skip this enrichment if the subject + is already self-explanatory, don't pad every line uniformly. + +### Step 5 — Write the file + +If the file doesn't exist: + +```markdown +# XCE Change Log + +Auto-generated by /xce-changes — component-grouped, newest week first. +`.wolf/` bookkeeping commits are excluded. + + +``` + +If it exists and this is a genuinely new window: insert the new section +immediately after the header block (title + intro lines), before the first +`## ` section, using Edit — preserve every existing section byte-for-byte. + +If this is a refresh of an existing window's section (Step 2): replace only +that `## ... ... ` block (up to the next `## ` or EOF) +with the newly generated content, via Edit — do not touch any other +section. + +### Step 6 — Report back + +Tell the user: the window covered, the output path, and a one-line count +per component (e.g. "controller 4, scheduler 9, docs 2 — 15 commits total, +3 bookkeeping excluded"). If the window had zero real commits, say so +plainly and ask whether the offset/range was intended. + +## Safety & Fallback + +**Not a git repository** — report that git evidence is unavailable; this +skill has no non-git fallback (unlike `/weekly_report`, there's no narrative +substitute for a mechanical commit ledger). + +**Existing `CHANGES.md` doesn't look auto-generated** (no `` +markers, or a title other than `# XCE Change Log`) — stop and ask before +editing; it may be a hand-maintained file this skill shouldn't clobber. + +**`--component` with no matching commits** — still report zero for that +component rather than leaving the subsection stale/unmentioned. + +## After Running + +Per the OpenWolf protocol, append a one-line entry to `.wolf/memory.md` +recording the change-log update, and if `CHANGES.md` didn't previously +exist, add it to `.wolf/anatomy.md`.