feat(skills): add xce-changes skill for component-grouped CHANGES.md

Generates/updates a rolling per-component change log from git history,
mirroring /weekly_report's date-window and continuity-check patterns but
organized by component (controller/worker/scheduler/etc.) instead of a
narrative weekly write-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-08-30 14:08:57 +00:00
parent 22aa16bf06
commit ccef561031
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2

View file

@ -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 <name>] [--output <path>]
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 <name>` | *(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 <path>` | `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 `<!-- window: START..END -->` 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 <name>` 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
## <HEADER>
<!-- window: <START>..<END> -->
### controller
- `<sha7>` <subject> (<date>)
### scheduler
- `<sha7>` <subject> (<date>)
...
```
- 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.
<new section>
```
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 `## ... <!-- window: ... --> ... ` 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 `<!-- window: -->`
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`.