Changelog automation with agent skills§
I am on my 4th globforsaken attempt to automate the Pigweed changelog. The latest attempt is powered by an agent skill. This blog post is a snapshot of my first attempt at extensive docs automation with agent skills.
Goals, requirements, constraints, etc.§
When a changelog is just a chronological dump of all commits over a given timeframe, then changelog automation is a solved problem. It’s easy to script everything. I have a bunch of goals, requirements, constraints, etc. that make the changelog automation process more challenging:
Publish monthly§
We publish a changelog at the start of every month.
End-to-end automation§
Pigweed has a small core team. I’m the only technical writer. If creating changelogs is time/energy intensive, the reality is that it’s not going to happen when I’m away. This is exactly what happened in 2025, when I went on paternity leave. Conversely, if any teammate can fire off a single command to get a changelog that’s pretty much ready to publish, then we have a decent chance of keeping up the monthly cadence.
“Stories”, not commits§
The upstream Pigweed repo gets hundreds of commits every month. Covering every commit in the changelog individually would be noisy and boring. Instead, the changelog should group related commits into a “story” and then summarize that larger body of work.
High signal-to-noise ratio§
The changelog updates should only publish stories with high user-facing impact. “Users” in this context are software engineers working in downstream firmware projects that depend on Pigweed. The most impactful stories should somehow float to the top of each changelog update. Determing what stories are “most impactful” is a squishy process full of heuristics.
Enjoyable§
I want Pigweed users to look forward to reading the monthly changelog.
Comprehensive§
I need strong guarantees that the message and diff of every commit is analyzed.
Walkthrough§
The cool thing about agent skills is that they really are mostly
self-documenting. The best way to understand this changelog automation is to
walk through the damn SKILL.md file itself, step-by-step.
Activate the skill§
---
name: changelog
description: Update the Pigweed changelog.
---
# Changelog update
This is the official process for updating the [Pigweed](https://pigweed.dev) changelog.
## General guidelines
The goal of the changelog is to highlight how Pigweed is progressing
and evolving. We do not attempt to comprehensively cover every change;
users can consult the Git commit log when they need that level of granularity.
The intended audience is a software engineer in a downstream project that
relies on Pigweed. Users should be able to enjoy reading a changelog
update during their morning coffee or commute.
Do not attempt to create scripts to speed up this process.
You must process commits in small batches, as specified in this document,
to ensure that each commit is properly analyzed.
To kick off the changelog automation, I provide a prompt like this to the agent:
create a changelog update for january 2026
Antigravity (the only agent product I’m testing against) automatically
detects that this skill is relevant to the prompt based on the YAML front
matter. It then loads the full SKILL.md and starts following the process.
Re: the do not attempt to create scripts guideline, I have found that these
agents are always looking for ways to finish the work as quickly as possible.
Their shortcut of choice is to spin up a one-off script that completely ignores
the process that I’ve defined. Telling them explicitly not to do this
reduces the tendency a little, but it’s not a robust solution.
Dummyproof the skill development process§
## 1. Output version
Inform the user that you're running the 20260304 version of the changelog automation.
My workflow for iteratively developing this automation went like this:
Make a bunch of tweaks to
SKILL.mdKick off the automation
Wait 15 minutes (or more) to get enough new generated output to determine if the tweaks are working
Get confused about why the agent was making all of the same mistakes as before
Realize that I hadn’t saved my changes to
SKILL.md
Don’t guess at the month and year§
## 2. Get the year and month
If the user hasn't specified a year and month, prompt them
to do so now. Do not guess.
In subsequent steps, `<YYYY>` should always be replaced with the
user-specified year and `<MM>` with the user-specified month.
With this instruction, Antigravity reliably prompts me to specify exactly what month and year we’re generating a changelog for when I don’t provide that data upfront.
Agents seem to understand placeholders like <YYYY> and <MM> well.
Placeholders keep subsequent instructions much tidier.
Initialize data§
## 3. Start
Run this command:
```
bazelisk run //.agents/skills/changelog/scripts -- start --year=<YYYY> --month=<MM>
```
This command makes sure that data files are properly populated. If there
are errors, stop here and inform the user of the problem(s). You can provide
this context to the user:
* `categories.json` error: There's a Pigweed module, third-party integration, or
target that's not accounted for.
This is where I start leaning heavily on the scripts feature of agent
skills. The start command:
Initializes some temporary data files in the
resources[1] directoryMakes sure that the golden
categories.jsondata is fresh
TODO4§
## 4. Get the next commit
Run this command:
```
bazelisk run //.agents/skills/changelog/scripts -- next --year=<YYYY> --month=<MM>
```
Inspect the data that was written to `//.agents/skills/changelog/resources/next.json`.
If the `next` key is `null`, you have reached the
terminating condition. Proceed to Step 6 (Copyedit).
```
{"next": null}
```
If the `next` key contains data, it will be a list of commits. Use these
commits in step 5.
The next command is the heart of the automation.
TODO5§
## 5. Group the next commits into stories
For each commit in the `next` list, decide whether to:
1. Add the next commit to an existing story
2. Refactor an existing story into 2 or more stories, then add
the next commit to one of these stories
3. Create a new story
Strongly prefer option 1 or 2. Only pick option 3 as a last resort, when
you are certain that the commit is not related to any of the existing stories.
Update `//.agents/skills/changelog/resources/data.toml` with the new or modified story.
Story example:
```
# stories
[stories.rust.protobuf]
title = "Protocol buffer support in Rust"
body = """
Bazel toolchain declarations have been added for ``prost`` and
``protoc-gen-prost``, and a Rust target for the Perfetto trace
proto has been introduced.
"""
highlight = """
prost and Perfetto support have been added to the Bazel toolchain for Rust.
"""
score = 600
[stories.rust.protobuf.commits."7818487b04e22ebbf2fc477c770c563e9fc0b5ed"]
summary = "Create toolchains for generating protobuf code in Rust with prost"
[stories.rust.protobuf.commits."4b8659b954abdbe97b97677d26105ac6b2eb14f7"]
summary = "Generate Perfetto tracing protobufs in Rust"
[stories.rust.protobuf.commits."32b1cd6f04f3ad5497954fb647e44aa27a759523"]
summary = "Add prost crates for generating protobufs in Rust"
…
```
The final output for the example above will eventually look like this:
```
<section id="rust">
<h2>Rust</h2>
<section id="protobuf">
<h3>Protocol buffer support in Rust</h3>
<p>
Bazel toolchain declarations have been added for <code>prost</code> and
<code>protoc-gen-prost</code>, and a Rust target for the Perfetto trace
proto has been introduced.
</p>
<p>Commits:</p>
<ul>
<li>
<a href="https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/366895">
pw_toolchain: Declare prost toolchains
</a>
</li>
<li>
<a href="https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/365978">
perfetto: Add Rust target for trace proto
</a>
</li>
<li>
<a href="https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/365977">
crates_io: Add prost crates to crates_std
</a>
</li>
</ul>
</section>
</section>
```
The `rust` part in `stories.rust.protobuf` is the category.
The `protobuf` part is the story ID.
`title` is a summary of the story in 60 characters or less. Plaintext.
This will be presented in an `<h3>` node. The title usually does not need to
repeat the Pigweed module name because the category already makes it clear.
`body` is a short paragraph explaining the story in more detail.
Break lines at 80 characters. Use reStructuredText formatting, e.g.
double backticks for inline code. This will
be presented as a `<p>` node below the `title`.
`highlight` is a one-sentence summary of the story. This may be presented
at the top of the changelog update. `highlight` must not redundantly repeat
the information provided in `title`. In the example, notice how `title`
sums up the story (`Protocol buffer support in Rust`) and how `highlight`
provides different information (`prost and Perfetto support have been added to
the Bazel toolchain for Rust.`).
The `commits` object contains all commits related to this story. Each top-level
key in the object is a commit SHA. The `summary` field is a 1-sentence summary
of the commit. Do not use the first line of the commit message as the summary. Generate
a summary based on the entire commit message and diff. This `summary` field will not
be displayed in the changelog, we only use it for "debugging" purposes.
`url`, `date`, and `title` will be provided by an automated script; you do not need to enter them.
`score` represents the importance of the story. See the "Scoring criteria"
section below. If needed, you can adjust the scores of other stories
in order to account for the new story. For example, if another story previously
was scored `750`, but now it looks like a `600`, you are welcome to change the score
for that other story to `600`.
FYI: See @.agents/skills/changelog/examples/2026-02.toml for an example of a
finished `data.toml` file and @docs/sphinx/changelog/2026/02.toml for
an example of how that `data.toml` file gets exported. The logic
for converting `02.toml` into reStructuredText is handled in
@docs/sphinx/_extensions/changelog.py
### Refactoring stories
Regarding option 2 (refactor an existing story), there should never be a
story with a generic title like `API updates and cleanups`. A story
with a generic title like the following should always be refactored into smaller,
more specific stories.
```
[stories.pw_async2.api_updates]
title = "API updates and cleanups"
body = """
Futures that don't produce a value now use ``Poll<void>`` by default instead of
``Poll<ReadyType>``. A ``FutureValue`` helper has been added to generically
access ``Future::value_type`` across templates. Additionally, the ``OnceSender``
and ``OnceReceiver`` types have been removed, replaced by the new
``OptionalValueProvider`` which wraps a value in ``std::optional`` and supports
cancellation.
"""
highlight = """
``pw::async2`` received several API updates, including defaulting ``Poll`` to
``void``, adding ``OptionalValueProvider``, and removing the deprecated
``OnceSender``/``OnceReceiver`` types.
"""
```
### Scoring criteria
`score` must be an integer between `0` and `1000`. There will potentially
be many stories with a score of `0`. For every other number (`1` to `1000`)
there must only be one story with that score.
Keep in mind that Pigweed is a modular platform. Some modules are more important
and popular than others. It's possible to use `pw_kernel`, for example, without
depending on most of the rest of Pigweed.
The intended user (i.e. audience) of the changelog is a software engineer in
a downstream project that relies on Pigweed.
Explanation of scoring ranges:
* `0`: The story has no user-facing impact. 0% of users are likely to be affected.
Examples:
* An internal, non-public function was renamed.
* Docs-only changes. (This technically has user-facing impact but we do not
show documentation-only changes in the changelog.)
* Test-only changes. (Pigweed is extensively tested. Covering every
test-only change will generate a lot of noise.)
* Trivial build system fixes. (Again, this technically has user-facing impact,
but it's boring to read.)
* `1` to `250`: The story has trivial impact on downstream projects. Less than
10% of users are likely to be affected. Examples:
* An extra parameter with a default value was added to an unimportant function.
* A minor feature or bug fix was applied to an experimental or "work in progress" module.
* A completely new document was added to the pigweed.dev documentation.
* `251` to `500`: The story has minor impact on downstream projects. 11% to 25%
of users are likely to be affected. Examples:
* A helper function was added.
* A new configuration option was added to a commonly used module, but the
default behavior remains unchanged.
* A minor performance improvement was made to a utility function.
* `501` to `750`: The story has moderate impact on downstream projects. 26% to
75% of users are likely to be affected. Examples:
* A Hardware Abstraction Layer (HAL) module like `pw_i2c_rp2040` or a
third-party integration module like `pw_thread_freertos` was created.
* A moderately used public function or class is being deprecated.
* A new backend implementation for an existing core facade was added.
* `751` to `1000`: The story has high impact on downstream projects. 76%
or more of users are likely to be affected. Examples:
* A critical bug was fixed.
* A core Pigweed module like `pw_kernel` or `pw_async2` was created.
* An important function was added to an important Pigweed module.
* A refactor of a Pigweed module has improved performance and resource usage
by 100%.
* A widely used API was changed in a breaking, non-backward-compatible way.
* A fundamental, highly anticipated capability or paradigm was introduced.
See @.agents/skills/changelog/examples/2026-01.toml and
@.agents/skills/changelog/examples/2026-02.toml for scoring examples.
TODO6§
## 6. Copyedit
Copyedit `docs/sphinx/changelog/<YYYY>/<MM>.toml` to follow our writing style.
See @.agents/skills/changelog/examples/2026-01.toml and
@.agents/skills/changelog/examples/2026-02.toml for writing style examples.
### Use module names directly
Before:
```
The ``pw_interrupt`` module now includes a fake backend specifically for …
```
After:
```
``pw_interrupt`` now includes a fake backend specifically for …
```
### Remove subjective statements when the value is obvious
Assume that the reader is an experienced software engineer. When the
value of a change is obvious to this reader, omit the subjective statement.
Before:
```
``pw_interrupt`` now includes a fake backend specifically for
host-side unit testing. This backend allows tests to simulate being in an
interrupt context, enabling thorough verification of interrupt-safe logic on
host platforms.
```
After:
```
``pw_interrupt`` now includes a fake backend specifically for
host-side unit testing. This backend allows tests to simulate being in an
interrupt context.
```
TODO7§
## 7. End
Run this comand:
```
bazelisk run //.agents/skills/changelog/scripts -- end --year=<YYYY> --month=<MM>
```
Inform the user of the total processing time.
TODO8§
## 8. Build the docs
The `end` subcommand from the last step copies over the unique, new `data.toml` into
`docs/sphinx/changelog/<YYYY>/<MM>.toml`. (Information that can be easily retrieved,
like the commit message, is not checked in.) `docs/sphinx/_extensions/changelog.py`
will convert `docs/sphinx/changelog/<YYYY>/<MM>.toml` into a document during the
Sphinx build. This is all automated; no work is needed here.
Add the new `docs/sphinx/changelog/<YYYY>/<MM>.toml` to
`docs/sphinx/changelog/BUILD.bazel`.
Check `docs/sphinx/index.rst`. If the changelog update that you just created
is more recent than the update that the `docs-root-changelog` section is pointing
to, update the `include` and `ref` to point to the latest changelog update.
Run this comand:
```
bazelisk build //docs
```
If there are errors and they look related to the changelog update, fix them.
If there are unrelated errors, do not attempt to fix them.
Appendix: 4 globforsaken attempts§
I’ll update this later to describe my 3 previous approaches.