Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions skills.sh.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
"setup-formsg-ai-skills"
]
},
{
"title": "Product",
"description": "Product management workflows.",
"skills": [
"highlights",
"reportcard"
]
},
{
"title": "General",
"description": "Thinking and communication skills that work across any domain.",
Expand Down
4 changes: 3 additions & 1 deletion skills/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"./skills/general/caveman",
"./skills/general/grill-me",
"./skills/general/handoff",
"./skills/general/write-a-skill"
"./skills/general/write-a-skill",
"./skills/product/highlights",
"./skills/product/reportcard"
]
}
4 changes: 2 additions & 2 deletions skills/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
Skills are organized into bucket folders under `skills/`:

- `engineering/` — daily code work
- `productivity/` — daily non-code workflow tools
- `product/` — product management workflows

Every skill in `engineering/`, `productivity/`, or `misc/` must have a reference in the top-level `README.md` and an entry in `.claude-plugin/plugin.json`. Skills in `personal/`, `in-progress/`, and `deprecated/` must not appear in either.
Every skill in `engineering/`, `product/`, or `misc/` must have a reference in the top-level `README.md` and an entry in `skills/.claude-plugin/plugin.json`. Skills in `personal/`, `in-progress/`, and `deprecated/` must not appear in either.

Each skill entry in the top-level `README.md` must link the skill name to its `SKILL.md`.

Expand Down
10 changes: 9 additions & 1 deletion skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ No per-repo setup required — available immediately after installing the plugin
| [`/write-a-skill`](general/write-a-skill/SKILL.md) | Create new skills with proper structure and bundled resources |
| [`/humanizer`](general/humanizer/SKILL.md) | Remove signs of AI writing from text, making it more natural and human |

## Product skills

Product management workflows. No per-repo setup required. See [product/README.md](product/README.md).

| Skill | Description |
|-------|-------------|
| [`/highlights`](product/highlights/SKILL.md) | Scan a product's Slack, GitHub and Notion sources for a quarter's shipped work, then draft the `CHANGELOG.md` entry in that product's existing format |
| [`/reportcard`](product/reportcard/SKILL.md) | Add a quarter's team, cost and metric values to an OGP product report card by auditing the last recorded quarter, then asking the PM for each missing number |

## Engineering skills

Requires `/setup-formsg-ai-skills` per repo. See [engineering/README.md](engineering/README.md) for setup, the full skill list, and the recommended workflow.
f
26 changes: 26 additions & 0 deletions skills/product/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Product skills

Product management workflows. No per-repo setup required — available immediately after installing the plugin.

These skills operate on a product's own repository (for example the products.open.gov.sg content repo), so run them from that checkout rather than from here.

## Skills

| Skill | Description |
|-------|-------------|
| [`/highlights`](highlights/SKILL.md) | Scan a product's Slack channel, GitHub repo and Notion pages for a quarter's shipped work, then draft the `CHANGELOG.md` entry in that product's existing format |
| [`/reportcard`](reportcard/SKILL.md) | Add a quarter's team, cost and metric values to an OGP product report card by auditing the last recorded quarter, then asking the PM for each missing number |

## Order

Run them in this order — `/highlights` writes the story, `/reportcard` fills in
the numbers:

```
/highlights <product> → CHANGELOG.md entry
/reportcard <product> → reportcard.yml values
```

They are separate because the jobs differ: `/highlights` is broad retrieval and
judgment across several sources and wants a strong model, while `/reportcard` is
a mechanical append that runs in seconds and works offline.
73 changes: 73 additions & 0 deletions skills/product/highlights/CURATING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Curating

Turning a few hundred raw findings into the handful that belong on a public
page. Do this with every source visible at once — the ranking signal only
exists across sources.

## Rank by corroboration

**An item appearing in more than one source is almost always a highlight.**

A real example: a v4 encryption rollout was filed as a routine refactor by the
GitHub pass, because that is what the PR titles looked like. Slack showed it
tracked as a milestone with a success metric; the Notion log tracked it weekly
all quarter. Three appearances meant it was the biggest item of the quarter.
The ~190 dependency bumps appeared once each, in one source.

Ranking, strongest first:

1. In all sources, with a number attached
2. In Slack or the planning doc with an outcome — someone reported it worked
3. In the planning doc as an intent **and** in GitHub as merged code
4. GitHub only, user-visible
5. GitHub only, internal — almost never publish

## What each source is actually good for

| Source | Gives you | Blind to |
| --- | --- | --- |
| GitHub | What was built, exact dates | Whether anyone used it, anything without code |
| Slack | Outcomes, adoption, real numbers | Anything not worth announcing |
| Planning doc | Intent, and what was dropped | What actually shipped |

**GitHub alone produces a feature list with no evidence anything worked.**
Adoption numbers, training sessions, and school or agency onboarding leave no
code trace at all, yet they are often what moved the metrics on the report
card.

The planning doc is the only place that shows **descoped** work. Worth a look:
something dropped for a good reason is occasionally a better story than
something shipped.

## What to cut

- Dependency bumps, CI, refactors, test changes — usually 70% of merged PRs
- Bug fixes, unless the bug was widely felt or the fix is itself a feature
- Internal team matters: hiring, leave, tooling experiments, token costs
- Security vulnerability details. Note that hardening happened; do not
enumerate what was exploitable
- Anything you cannot trace to a source

## Writing it

- **Plain language.** Public officers read this, not the team. Internal names
get a plain-English gloss: an internal schema name is fine in parentheses if
the line explains what changed for users.
- **Do not claim what has not landed.** Something merged behind a feature flag
and announced next quarter belongs in next quarter's entry. Say "rolled out
to X" only if it was.
- **Keep numbers exactly as reported.** `27.7k submissions` not `~28k`.
Rounding a sourced figure makes it untraceable.
- Group under the same top-level headings the last entry used, even if the
fit is imperfect. Consistency across quarters beats a tidier taxonomy.

## Before handing over

State plainly:

- which sources were unreachable, and what may be missing as a result
- which items you cut that were arguable, so the PM can overrule you
- every placeholder, and where the number would come from

Never present a draft as complete when a source failed. Silence about a gap
reads as "there was nothing there".
82 changes: 82 additions & 0 deletions skills/product/highlights/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
name: highlights
description: Scan a product's Slack channel, GitHub repo and Notion pages for a quarter's shipped work, then draft a CHANGELOG.md entry in that product's existing format. Use when someone wants to write or draft quarterly highlights, gather what shipped last quarter, update a product CHANGELOG, or asks what to put in the changelog for Q1/Q2/Q3/Q4.
---

# Quarterly highlights

Gather what a product shipped in a quarter, decide the few things worth
publishing, and draft the `CHANGELOG.md` entry.

Does not touch `reportcard.yml` — that is `/reportcard`'s job. **Never invent a
metric, a date, or a feature.** Everything published must trace to something a
person actually wrote. A fabricated usage number on a public government page is
the worst outcome available here, and plausible numbers are the easiest thing
to generate by accident.

## 1. Prep

```bash
python3 scripts/prep.py <product> [--quarter q2-2026]
```

Prints the date range, the heading to add, and **the last entry verbatim** —
that is the format to mirror. Do not impose a house style; every product
formats differently and the file is the source of truth. It also lists the
metric labels that product uses (`Usage`, `Ease of use`, …).

## 2. Confirm the sources

Sources differ per product and are not recorded anywhere, so ask — but arrive
with guesses rather than a blank question:

- **GitHub** — try `gh repo view opengovsg/<Product>` and propose it if it exists
- **Slack** — search for a channel matching the product or its division
- **Notion** — ask; there is rarely a way to guess

Ask once, in one message, then confirm before scanning. Also ask if anything
lives elsewhere — a Jira board, a team wiki, a mailing list.

Worth knowing: teams often post through a **weekly digest bot** rather than
individually. Ask how updates usually reach the channel; it changes what to
search for.

## 3. Fan out — one subagent per source

Retrieval is mechanical, so run it in parallel on a cheaper tier and keep the
raw volume out of the main context. Each subagent gets the date range and
returns **raw findings only** — no judgment about importance:

- date, one-line description in the author's own words, link, any hard numbers
- an explicit "could not access X, because Y" rather than silence

Tell each one plainly: do not infer, do not fill gaps, accuracy over
completeness.

## 4. Curate

This is the step that cannot be parallelised — you cannot tell which items
matter until every source is visible at once. See
[CURATING.md](CURATING.md) for how to rank and what to cut.

## 5. Draft

Mirror the previous entry's structure exactly, down to the group names and
metric labels. Where a feature carried a metric last quarter and this
quarter's number is unknown, write:

```
- Usage: [Placeholder — Q1 reported 6.6k]
```

Quoting last quarter's value gives the PM a baseline — a number that has gone
flat after a feature left beta is worth noticing before it is published.

Write the draft to `.scratch/<product>-<quarter>-changelog-draft.md` for review.
Only paste it into `CHANGELOG.md` once the PM has filled or accepted the
placeholders.

## Model

Opus, high effort, for curation — choosing 10 items from 300 is judgment, not
retrieval. The fan-out subagents can run on Sonnet at medium.
102 changes: 102 additions & 0 deletions skills/product/highlights/scripts/prep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Prepare a highlights scan: quarter bounds + last changelog entry as the format template.

Stdlib only.

Usage:
python3 prep.py formsg # infers the quarter after the latest logged
python3 prep.py formsg --quarter q2-2026
Comment on lines +6 to +8
"""

import argparse
import re
import sys
from pathlib import Path

HEADING = re.compile(r"^##\s+Q([1-4])\s+(\d{4})\b(.*)$")
MONTHS = {1: ("Jan-Mar", "01", "03-31"), 2: ("Apr-Jun", "04", "06-30"),
3: ("Jul-Sep", "07", "09-30"), 4: ("Oct-Dec", "10", "12-31")}


def parse_quarter(s):
m = re.fullmatch(r"[qQ]?([1-4])[-_ /]?(\d{4})", s.strip()) or \
re.fullmatch(r"(\d{4})[-_ /]?[qQ]?([1-4])", s.strip())
if not m:
sys.exit(f"error: could not read quarter '{s}'. Try q2-2026.")
a, b = m.group(1), m.group(2)
return (int(a), int(b)) if len(b) == 4 else (int(b), int(a))


def main():
ap = argparse.ArgumentParser()
ap.add_argument("product")
ap.add_argument("--quarter", help="e.g. q2-2026")
ap.add_argument("--root", default=".", help="repo root")
args = ap.parse_args()

cl = Path(args.root) / "_products" / args.product / "CHANGELOG.md"
if not cl.exists():
sys.exit(f"error: {cl} not found -- check the product id")

lines = cl.read_text().splitlines()
entries = [(i, m) for i, l in enumerate(lines) if (m := HEADING.match(l))]
if not entries:
sys.exit(f"error: no '## Q<n> <year>' headings in {cl}")

last_i, last_m = entries[0]
last_q, last_y = int(last_m.group(1)), int(last_m.group(2))

if args.quarter:
q, y = parse_quarter(args.quarter)
else:
q, y = (last_q + 1, last_y) if last_q < 4 else (1, last_y + 1)

label, start_mm, end_md = MONTHS[q]
print(f"product : {args.product}")
print(f"target quarter : Q{q} {y} ({label})")
print(f"date range : {y}-{start_mm}-01 to {y}-{end_md}")
print(f"heading to add : ## Q{q} {y} {label}")
print(f"latest logged : Q{last_q} {last_y}")

if (y, q) == (last_y, last_q):
print(f"\n!! Q{q} {y} IS ALREADY IN THE CHANGELOG -- check before adding another.")
gap = (y - last_y) * 4 + (q - last_q)
if gap > 1:
print(f"\n!! GAP: {gap - 1} quarter(s) missing between Q{last_q} {last_y} and target.")

# ---- last entry, verbatim: this IS the format to mirror -----------------
end = entries[1][0] if len(entries) > 1 else len(lines)
block = lines[last_i:end]

# Only count labels that sit inside a "- Metrics" block, i.e. indented
# deeper than the Metrics bullet itself.
labels, metrics_indent = set(), None
for l in block:
if not l.strip():
continue
indent = len(l) - len(l.lstrip())
if re.match(r"^\s*-\s+Metrics\s*:?\s*$", l):
metrics_indent = indent
continue
if metrics_indent is None:
continue
if indent <= metrics_indent:
metrics_indent = None
continue
if m := re.match(r"^\s*-\s+([A-Za-z][A-Za-z ()]*?)\s*:", l):
labels.add(m.group(1).strip())
labels = sorted(labels)
groups = [l.strip()[2:] for l in block if re.match(r"^-\s+\S", l)]

print(f"\ntop-level groups used last quarter: {groups}")
print(f"metric labels used last quarter : {labels}")
print("\nIf a feature carried a metric last quarter, it needs one this quarter --")
print("a real value or an explicit [Placeholder]. Never a guess.")
print("\n" + "=" * 70)
print("LAST ENTRY VERBATIM -- mirror this structure exactly")
print("=" * 70)
print("\n".join(block).rstrip())


if __name__ == "__main__":
main()
Loading