diff --git a/.agents/skills/create-pr/SKILL.md b/.agents/skills/create-pr/SKILL.md
index 703609c9a1..f285574bb8 100644
--- a/.agents/skills/create-pr/SKILL.md
+++ b/.agents/skills/create-pr/SKILL.md
@@ -75,10 +75,23 @@ description structure, writing principles) and `mem:workflow/creating-commits`
(commit type emojis). Derive the title and body from the commits and, when
there is one, from the issue body. Reference the issue with `Closes #NNNN`.
+Repeat the `AI-assisted-by:` trailer in the body, once per model that worked
+on the branch, so the PR states the assistance where a reviewer reads it. The
+branch commits keep their own trailers, and a squash merge carries every one
+of them into the landed message.
+
+Before offering or accepting a draft PR, warn that CI doesn't run on them. Add
+`--draft` only when the user agrees to that.
+
```bash
-gh pr create --repo penpot/penpot --title "
" --body-file /tmp/pr-body.md
+gh pr create --repo penpot/penpot --base "" --title "" \
+ --project "Main" --body-file /tmp/pr-body.md
```
+`--base` is the branch resolved in step 1: without it the PR opens against the
+repository default, which is wrong for a branch cut from `staging`. `--project
+"Main"` is required by `mem:workflow/creating-prs`.
+
### 5. Report
Report the PR URL and stop.
diff --git a/.agents/skills/update-changelog/SKILL.md b/.agents/skills/update-changelog/SKILL.md
index d482959f94..5fa2883d6f 100644
--- a/.agents/skills/update-changelog/SKILL.md
+++ b/.agents/skills/update-changelog/SKILL.md
@@ -357,6 +357,39 @@ Insert the new version section right after the `# CHANGELOG` header (before
the previous version entry). Use the `edit` tool with enough context to make
a unique match.
+### 8b. Propose and populate the `:rocket: Epics and highlights` subsection
+
+After inserting the version section, proactively create or populate the
+`### :rocket: Epics and highlights` subsection. This section surfaces the
+most impactful changes for self-hosted users checking for updates.
+
+**When to create:** If the version section does not already have a
+`### :rocket: Epics and highlights` subsection, create one. Place it before
+`### :sparkles:` (matching existing order in CHANGES.md).
+
+**How to identify highlights:** Review the `:sparkles:` entries for the
+version and select 2–5 of the most impactful/user-visible ones. Criteria:
+- New user-visible features (not internal refactors)
+- Significant capability additions
+- Items that create "FOMO" for self-hosted users on older versions
+
+**Use release notes as hints:** Check
+`frontend/src/app/main/ui/releases/v2_.cljs` for the corresponding
+version. The slide titles and feature descriptions there are curated
+marketing content indicating what the team considers highlight-worthy. Match
+those themes to changelog entries. Treat these files as optional hints — they
+may not exist for every version.
+
+**Format requirement:** Every `:rocket:` entry MUST follow the standard
+changelog format with issue/PR references:
+```
+- [#](https://github.com/penpot/penpot/issues/) (PR: [#](https://github.com/penpot/penpot/pull/))
+```
+An entry without issue AND PR references is a highlight gap (warning, not an anomaly).
+
+**Preserve existing entries:** If the `:rocket:` section already exists from
+a prior run, preserve its entries. Do not remove or rewrite them.
+
### 9. Verify
Read the top of `CHANGES.md` and confirm:
@@ -468,9 +501,8 @@ Markdown viewer.
## What is an anomaly
**An anomaly is a milestone-mismatch between an issue and its referenced
-PR.** It indicates that the changelog claim "this issue is fixed by this PR,
-all in milestone M" is inconsistent with the actual milestone assignments.
-There are exactly two types:
+PR.** There are two anomaly types, plus two highlight gaps (warnings that
+do not count toward the anomaly total):
1. **Issue is in the milestone, but its referenced PR is in a different
milestone (or has no milestone).** The changelog claims a fix in this
@@ -486,6 +518,13 @@ There are exactly two types:
PR that closes an issue with no milestone references an issue from
another (probably private) project; that is expected and the issue is
not part of this changelog. Do not report it.
+3. **missing-highlights (gap):** A released X.Y.0 version section has no
+ `### :rocket: Epics and highlights` subsection. Patches (X.Y.Z) never
+ carry highlights, so only minors/majors are checked.
+4. **missing-highlight-reference (gap):** A `:rocket:` entry lacks the
+ required issue AND PR references. Every highlight entry must follow the
+ standard changelog format with `[#ISSUE]` and `(PR: [#PR])` links
+ (multi-PR `(PR: [#A](...), [#B](...))` accepted).
**Anything else is not an anomaly.** Other discrepancies (exclusion
labels on in-changelog issues, missing valid issues, unmerged PR
@@ -653,6 +692,40 @@ for pr_num in sorted(changelog_prs):
'issue_milestone': issue_ms, # may be None
})
+# --- Type C: released X.Y.0 version sections without :rocket: subsection ---
+# Patches (X.Y.Z with Z != 0) never carry :rocket: by design — only minors/majors (X.Y.0).
+anomalies_c = [] # list of version strings
+rocket_heading_re = re.compile(r'^### :rocket:', re.MULTILINE)
+version_sections = re.split(r'(?=^## \d+\.\d+\.\d+)', content, flags=re.MULTILINE)
+for vs in version_sections:
+ m = re.match(r'^## (\d+\.\d+\.\d+)(.*)', vs)
+ if not m: continue
+ ver, suffix = m.group(1), m.group(2)
+ if 'unreleased' in suffix.lower(): continue
+ if ver.split('.')[2] != '0': continue
+ if not rocket_heading_re.search(vs):
+ anomalies_c.append(ver)
+
+# --- Type D: :rocket: entries without issue AND PR references ---
+# Both are required: `[#ISSUE](.../issues/N)` and `(PR: [#PR](.../pull/M))`.
+# Multi-PR entries `(PR: [#A](...), [#B](...))` are accepted.
+anomalies_d = [] # list of dicts: {version, line}
+issue_ref_re = re.compile(r'\[#\d+\]\(https://github\.com/penpot/penpot/issues/\d+\)')
+pr_ref_re = re.compile(r'\(PR:\s*\[#\d+\]\(https://github\.com/penpot/penpot/pull/\d+\)(\s*,\s*\[#\d+\]\(https://github\.com/penpot/penpot/pull/\d+\))*\)')
+for vs in version_sections:
+ m = re.match(r'^## (\d+\.\d+\.\d+)(.*)', vs)
+ if not m: continue
+ ver = m.group(1)
+ rocket_match = rocket_heading_re.search(vs)
+ if not rocket_match: continue
+ # Extract the :rocket: subsection body (up to next ### or ##)
+ rocket_body = vs[rocket_match.end():]
+ rocket_body = re.split(r'(?m)^#{2,3}\s', rocket_body)[0]
+ for line in rocket_body.splitlines():
+ line = line.strip()
+ if line.startswith('- ') and not (issue_ref_re.search(line) and pr_ref_re.search(line)):
+ anomalies_d.append({'version': ver, 'line': line[:100]})
+
# --- Write report ---
def fmt_ms(ms):
return ms if ms else "_none_"
@@ -664,13 +737,17 @@ with open(OUTPUT, 'w') as f:
n_a = len(anomalies_a)
n_b = len(anomalies_b)
+ n_c = len(anomalies_c)
+ n_d = len(anomalies_d)
f.write('## Summary\n\n')
f.write(f'- **Issue in {MILESTONE}, referenced PR in different milestone or no milestone:** {n_a}\n')
f.write(f'- **PR in {MILESTONE}, closing issue in a different milestone:** {n_b}\n')
- f.write(f'- **Total anomalies:** {n_a + n_b}\n\n')
+ f.write(f'- **Total anomalies:** {n_a + n_b}\n')
+ f.write(f'- **Released X.Y.0 version missing :rocket: section (gap):** {n_c}\n')
+ f.write(f'- **:rocket: entry without issue AND PR references (gap):** {n_d}\n\n')
- # --- Anomalies section ---
+ # --- Anomalies section (milestone mismatches only) ---
if n_a or n_b:
f.write('## Anomalies\n\n')
f.write('These are milestone mismatches between an issue in the changelog '
@@ -709,9 +786,37 @@ with open(OUTPUT, 'w') as f:
badge = '🔴' if e['issue_milestone'] is None else '⚠️'
f.write(f' - {badge} Closing {issue_link(e["issue"])} is in milestone **{ms_label}** (expected: {MILESTONE})\n')
f.write('\n')
+
else:
f.write('✅ No anomalies found. All (issue, PR) pairs in the changelog have aligned milestone assignments.\n\n')
+ # --- Highlight gaps (warnings, not anomalies) ---
+ if n_c or n_d:
+ f.write('## Highlight gaps\n\n')
+ f.write('These are warnings, not anomalies: they do not affect the '
+ 'milestone-mismatch total above. They track `:rocket:` coverage '
+ 'across all released X.Y.0 versions. Historical entries (e.g. '
+ 'Taiga links) predate the current reference convention and are '
+ 'expected to appear here.\n\n')
+
+ if n_c:
+ f.write(f'### Released X.Y.0 version missing :rocket: section\n\n')
+ f.write('These released minors/majors have no `### :rocket: Epics and highlights` subsection. '
+ 'Add highlights to help self-hosted users understand what they are missing.\n\n')
+ for ver in anomalies_c:
+ f.write(f'- Version **{ver}**\n')
+ f.write('\n')
+
+ if n_d:
+ f.write(f'### :rocket: entry without issue AND PR references\n\n')
+ f.write('These highlight entries lack the required issue AND PR references. '
+ 'Add `[#ISSUE](...)` and `(PR: [#PR](...))` links.\n\n')
+ for d in anomalies_d:
+ f.write(f'- **{d["version"]}**: `{d["line"]}`\n')
+ f.write('\n')
+ elif not (n_a or n_b):
+ f.write('✅ No highlight gaps found. All released X.Y.0 versions have properly referenced :rocket: entries.\n\n')
+
# --- Context ---
f.write('---\n\n')
f.write('## Context\n\n')
@@ -726,8 +831,7 @@ print(f"Anomaly report written to {OUTPUT}")
PYEOF
```
-This generates `CHANGES-ISSUES.md` containing **only the anomalies** —
-milestone mismatches between issues and their referenced PRs:
+This generates `CHANGES-ISSUES.md` containing anomalies and highlight gaps:
1. **Issue in milestone, referenced PR in different milestone or no milestone** —
the changelog claims a fix here, but the PR is released elsewhere.
@@ -736,6 +840,13 @@ milestone mismatches between issues and their referenced PRs:
(An issue with *no* milestone belongs to another, probably private,
project — milestones are only required on the "Main" project — so it is
neither an anomaly nor a changelog candidate.)
+3. **missing-highlights (gap, warning)** — a released X.Y.0 version section
+ has no `### :rocket: Epics and highlights` subsection. Patches (X.Y.Z)
+ never carry highlights.
+4. **missing-highlight-reference (gap, warning)** — a `:rocket:` entry lacks
+ the required issue AND PR references.
+
+Gaps do not count toward the anomaly total.
**Rule violations are not in the report** — they are workflow errors the
LLM must fix directly in `CHANGES.md` during step 6a (pre-flight checks).
@@ -809,10 +920,13 @@ self-contained and clickable in any Markdown viewer.
issue from a different project or context. If the PR title and issue title
are clearly unrelated, or the PR predates the issue by years, treat it as a
data glitch and skip it.
-- **Anomaly = milestone mismatch only.** The report contains only milestone
- mismatches: (1) the issue is in this milestone but the referenced PR is
- in a different milestone (or unassigned), and (2) the PR is in this
- milestone but the issue it closes is in a different milestone. An
+- **Anomaly = milestone mismatch only; gaps are warnings.** The report's
+ anomaly total counts only milestone mismatches: (1) the issue is in this
+ milestone but the referenced PR is in a different milestone (or unassigned),
+ and (2) the PR is in this milestone but the issue it closes is in a
+ different milestone. `:rocket:` highlight gaps (missing section on a
+ released X.Y.0, entry without issue AND PR references) are reported in a
+ separate `Highlight gaps` section and never count toward the anomaly total. An
*unassigned* (milestone-less) issue closed by a milestone PR is **not**
an anomaly: milestones are required only for the "Main" project, so such
issues come from another (probably private) project and are not changelog