Keeps the project's history/evolution docs (changelog, narrative timeline, state snapshot, lessons, session wrapup) up to date after a significant session — with built-in degradation (a stamp instead of endless narrative) from day 1.
Installs into .claude/skills of the current project.
Are you the author of Doc Rollup?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/rusharlabs-doc-rollup)
---
name: doc-rollup
description: Keeps the project's history/evolution docs (changelog, narrative timeline, state snapshot, lessons, session wrapup) up to date after a significant session — with built-in degradation (a stamp instead of endless narrative) from day 1.
---
> **Auto-Trigger:** When closing a significant wave/feature, before a `/pre-clear`, or when the operator asks "update the docs"/"record this in the history".
> **Keywords:** "doc-rollup", "update changelog", "update history", "docs rollup", "session wrapup", "record in history", "stamp", "update everything"
> **Priority:** MEDIUM
> **Tools:** Read, Bash
## When NOT to Activate
- Session with nothing significant to record (pure reading/exploration) — do not force an empty entry.
- Do not confuse with the `pre-clear` skill, which covers the SHORT TERM (what to do in the next session — handoff). This skill covers the LONG TERM (how we got here — history). `pre-clear` calls this skill as one of its steps, conditionally.
- Missing config (`rollup.yaml` does not exist) — copy `rollup.example.yaml` first; the skill does not invent targets.
## Contract
**INPUT:** JSON payload via stdin, assembled by the model (only it knows what happened in this session): `session_marker` (unique string, mandatory — used to detect a session that was already recorded, so nothing is inserted twice), `summary` (mandatory), `date` (optional, default today BRT), `shipments[]`, `metrics[]` (each item NEEDS a `re_derive_cmd` — every number needs a command to re-derive it live), `decisions[]`, `aprendizados[]`. Config in `rollup.yaml` (copied from `rollup.example.yaml`).
**OUTPUT:** per-target JSON report (`{path, mode, role, status, ...}`) printed to stdout. Target files updated on disk (`plan` mode does not touch the disk).
**EXIT CODES:**
| Exit | Meaning |
|---|---|
| 0 | processed (even with targets skipped for collision/passive — that is correct behavior) |
| 1 | payload rejected (`session_marker`/`summary` missing, or a metric without `re_derive_cmd`) |
| 2 | invalid usage (missing config, malformed JSON, `--stdin` not passed) |
**STATE IT TOUCHES:**
| Resource | Reads/Writes | Purpose |
|---|---|---|
| `rollup.yaml` | Reads | target config (paths, mode, template, thresholds) |
| each `target.path` (resolved) | Reads+Writes (inserts/appends, never overwrites the whole file) | the history doc itself |
| `<path>.bak-rollup-<timestamp>` | Writes | backup before any edit to an existing file |
| paths with `passive: true` | NEVER touches | defense in depth (e.g. a mirror auto-generated by another mechanism) |
## Process
1. **Copy the config once per project:** `cp rollup.example.yaml rollup.yaml` and adjust the real targets (file names, templates, thresholds).
2. **Assess significance:** did something close in this session that deserves a record? If not, skip and record why (do not force the rollup).
3. **Preview before applying** (optional, but recommended — does not touch the disk):
```bash
echo '<json>' | python ${CLAUDE_PLUGIN_ROOT}/scripts/doc_rollup.py plan --stdin --config rollup.yaml
```
4. **Apply for real:**
```bash
echo '<json>' | python ${CLAUDE_PLUGIN_ROOT}/scripts/doc_rollup.py apply --stdin --config rollup.yaml
```
5. **Read the per-target report.** `applied:*`/`created` = wrote; `skipped-collision` = that session was already there (not duplicated, correct); `skipped-passive` = protected target (it should never write there anyway); `applied:stamp-degrade` = the target crossed the threshold and received only a 1-line stamp instead of the full narrative (expected, not an error).
6. **If `pre-clear` is installed**, it calls this step automatically BEFORE the handoff (to capture the post-rollup file tree in the handoff).
## Executed examples
```console
$ echo '{"session_marker":"doc-ex-001","date":"2026-07-10","summary":"doc-rollup example","shipments":["rollup.yaml","doc_rollup.py"]}' | python scripts/doc_rollup.py apply --stdin --config rollup.yaml
{
"repos": [
{
"name": "example",
"targets": [
{
"path": "CHANGELOG.md",
"mode": "prepend-after-header",
"status": "applied:prepend",
"content_preview": "## [2026-07-10] doc-rollup example\n\n- rollup.yaml\n- doc_rollup.py\n"
}
]
}
]
}
```
<!-- executed: 2026-09-21 · exit=0 -->
```console
$ echo '{"shipments":["x"]}' | python scripts/doc_rollup.py apply --stdin --config rollup.yaml
{
"status": "rejected",
"errors": [
"session_marker missing (required for collision/idempotency detection — LC-4)",
"summary missing"
]
}
```
<!-- executed: 2026-09-21 · exit=1 -->
(a payload without `session_marker`/`summary` is REJECTED before touching any file.)
```console
$ echo '{"session_marker":"doc-ex-001","date":"2026-07-10","summary":"attempt to re-insert the same session","shipments":["x"]}' | python scripts/doc_rollup.py apply --stdin --config rollup.yaml
{
"repos": [
{
"name": "example",
"targets": [
{
"path": "CHANGELOG.md",
"status": "skipped-collision",
"reason": "session_marker 'doc-ex-001' already present in this target (LC-4: do not re-insert)"
}
]
}
]
}
```
<!-- executed: 2026-09-21 · exit=0 -->
(the SAME `session_marker` again does not duplicate the entry — real idempotency, not just documented.)
## Anti-patterns
- ❌ Writing the history file by hand instead of running `doc_rollup.py` — you lose backup, collision detection and automatic degradation.
- ❌ Treating `skipped-collision`/`skipped-passive` as an "error" — they are the guardrail working, not a failure to fix.
- ❌ Letting a manual narrative doc grow without `max_bytes`/`max_entries` configured — that is exactly the decay this mechanism exists to prevent from day 1.
## Proof
```bash
python ${CLAUDE_PLUGIN_ROOT}/scripts/doc_rollup.py --self-test
```