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:
parent
22aa16bf06
commit
ccef561031
1 changed files with 282 additions and 0 deletions
282
.claude/skills/xce-changes/SKILL.md
Normal file
282
.claude/skills/xce-changes/SKILL.md
Normal 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`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue