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>
282 lines
11 KiB
Markdown
282 lines
11 KiB
Markdown
---
|
||
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`.
|