myxce/.claude/skills/xce-changes/SKILL.md
Rongsong Shen ccef561031
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>
2026-08-30 14:08:57 +00:00

11 KiB
Raw Blame History


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 <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).

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

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

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):

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

## <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:

# 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.