@ericrisco/rsc 1.0.24 → 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/manifest.json +3 -2
- package/package.json +1 -1
- package/scripts/doctor.js +3 -0
- package/scripts/install-apply.js +1 -0
- package/skills/constitution/references/constitution-template.md +1 -1
- package/skills/design/SKILL.md +31 -1
- package/skills/design/evals/cases.yaml +1 -1
- package/skills/design/references/ai-tells.md +75 -0
- package/skills/design/references/design-systems.md +41 -0
- package/skills/design/references/motion-and-interaction.md +35 -0
- package/skills/design/scripts/verify.sh +116 -10
- package/skills/git-workflow/SKILL.md +36 -12
- package/skills/git-workflow/evals/cases.yaml +5 -0
- package/skills/git-workflow/references/gitmoji.md +157 -0
- package/skills/ship/SKILL.md +2 -1
- package/targets/claude.js +16 -1
- package/targets/gitmoji-guard.mjs +250 -0
package/README.md
CHANGED
|
@@ -119,6 +119,11 @@ also wires a `SessionStart` hook (so your assistant proposes new skills on its
|
|
|
119
119
|
own) and a `UserPromptSubmit` hook that re-asserts the **SDD new-feature gate**
|
|
120
120
|
on every turn — so a feature request, in any language, routes through `specify`
|
|
121
121
|
first, before any skill writes code. Opt out per project with `.rsc/.no-feature-gate`.
|
|
122
|
+
It also wires a `PreToolUse` **gitmoji guard**: a `git commit` whose message carries no
|
|
123
|
+
[gitmoji](https://gitmoji.dev) is denied, with the corrected message inside the refusal
|
|
124
|
+
(`✨ feat(api): add cursor paging`) — so `git log --oneline` stays readable at a glance instead of
|
|
125
|
+
depending on the agent remembering a convention. It only judges messages it can actually read
|
|
126
|
+
(`-m`, heredoc), never an editor commit or `--amend --no-edit`. Opt out with `.rsc/.no-gitmoji`.
|
|
122
127
|
|
|
123
128
|
Everything stays **in the project**, and the real skill files are written
|
|
124
129
|
**once** to `.rsc/skills/<id>/`. Each assistant you pick gets a lightweight
|
package/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.
|
|
2
|
+
"version": "1.0.26",
|
|
3
3
|
"counts": {
|
|
4
4
|
"skills": 264
|
|
5
5
|
},
|
|
@@ -2114,11 +2114,12 @@
|
|
|
2114
2114
|
},
|
|
2115
2115
|
{
|
|
2116
2116
|
"id": "git-workflow",
|
|
2117
|
-
"description": "Use when naming or scoping a branch, writing or fixing a commit message, untangling history (rebase versus merge versus squash), or cutting a versioned release — the portable git-convention layer for any repo. Covers Conventional Commits, SemVer tags, branch hygiene, force-push safety and gh pr/release mechanics. NOT the land-it decision and pre-ship checklist (that is `ship`), NOT an isolated checkout before coding (that is `worktrees`), NOT CI/CD release automation (that is `deployment`).",
|
|
2117
|
+
"description": "Use when naming or scoping a branch, writing or fixing a commit message, picking the gitmoji for a commit, untangling history (rebase versus merge versus squash), or cutting a versioned release — the portable git-convention layer for any repo. Covers gitmoji + Conventional Commits, SemVer tags, branch hygiene, force-push safety and gh pr/release mechanics. NOT the land-it decision and pre-ship checklist (that is `ship`), NOT an isolated checkout before coding (that is `worktrees`), NOT CI/CD release automation (that is `deployment`).",
|
|
2118
2118
|
"tags": [
|
|
2119
2119
|
"git",
|
|
2120
2120
|
"version-control",
|
|
2121
2121
|
"conventional-commits",
|
|
2122
|
+
"gitmoji",
|
|
2122
2123
|
"semver",
|
|
2123
2124
|
"pull-request"
|
|
2124
2125
|
],
|
package/package.json
CHANGED
package/scripts/doctor.js
CHANGED
|
@@ -57,6 +57,9 @@ export function doctor({ target, home, cwd }) {
|
|
|
57
57
|
},
|
|
58
58
|
contextBudget: contextBudget({ target, home, cwd }),
|
|
59
59
|
sello: selloStatus(root),
|
|
60
|
+
// An inert gate must never read as an armed one (the same rule the sello follows):
|
|
61
|
+
// the gitmoji guard has a per-project kill switch, so say which state it is in.
|
|
62
|
+
gitmojiGuard: existsSync(join(root, '.rsc', '.no-gitmoji')) ? 'opted-out' : 'armed',
|
|
60
63
|
// Counted, never interpreted — by spec, the gap log's reader is the user.
|
|
61
64
|
automationGaps: countGaps(root),
|
|
62
65
|
};
|
package/scripts/install-apply.js
CHANGED
|
@@ -60,6 +60,7 @@ function generatedHookFiles({ target, cwd }) {
|
|
|
60
60
|
join(cwd, '.rsc', 'worklog-checkpoint.mjs'),
|
|
61
61
|
join(cwd, '.rsc', 'ship-guard.mjs'),
|
|
62
62
|
join(cwd, '.rsc', 'danger-guard.mjs'),
|
|
63
|
+
join(cwd, '.rsc', 'gitmoji-guard.mjs'),
|
|
63
64
|
join(cwd, '.rsc', 'userprompt-gate.mjs'),
|
|
64
65
|
join(cwd, '.rsc', 'hook-once.mjs'),
|
|
65
66
|
join(cwd, '.rsc', 'sello.mjs'),
|
|
@@ -40,7 +40,7 @@ version: v1.0.0
|
|
|
40
40
|
|
|
41
41
|
7. Naming & structure: <directory layout, module boundaries, file naming>.
|
|
42
42
|
8. API & errors: <error shape, status codes, response envelope> where applicable.
|
|
43
|
-
9. Commit messages: <format, e.g. Conventional Commits
|
|
43
|
+
9. Commit messages: <format, e.g. gitmoji + Conventional Commits — `✨ feat(scope): subject`>.
|
|
44
44
|
|
|
45
45
|
## 4. Branching & shipping
|
|
46
46
|
|
package/skills/design/SKILL.md
CHANGED
|
@@ -234,6 +234,36 @@ img { outline: 1px solid oklch(0 0 0 / 0.1); outline-offset: -1px; }
|
|
|
234
234
|
|
|
235
235
|
Depth recipes, glass, noise, concentric math → `references/visual-system.md`.
|
|
236
236
|
|
|
237
|
+
## The three locks, and who checks them
|
|
238
|
+
|
|
239
|
+
A long page generated in passes drifts against itself. Three things must be **one** thing for
|
|
240
|
+
the whole page, and they are not preferences — a page that breaks one of them is defective:
|
|
241
|
+
|
|
242
|
+
1. **Theme lock.** One theme for the page (light, dark, or auto). No section flipping to
|
|
243
|
+
inverted halfway down because it looked good alone.
|
|
244
|
+
2. **Accent lock.** One accent, used identically in every section. **This one is judgement**:
|
|
245
|
+
deciding which token is the accent needs to read the system's intent, so no script claims
|
|
246
|
+
to check it — you do, before you claim done.
|
|
247
|
+
3. **Radius lock.** One radius system. Four different `rounded-*` scales in one tree means
|
|
248
|
+
there is no system, and `scripts/verify.sh` counts them.
|
|
249
|
+
|
|
250
|
+
## The tells the script owns, and the ones you own
|
|
251
|
+
|
|
252
|
+
`scripts/verify.sh` carries a **registry of mechanical tells** and reports each with file and
|
|
253
|
+
line: dashes in rendered copy, scroll cues, numbered eyebrows, build stamps on marketing
|
|
254
|
+
pages, unstable viewport heights, middot chains, hype words, plus counted ratios (eyebrows
|
|
255
|
+
against sections, double-bordered rows, radius scales). Run it; do not re-derive that list
|
|
256
|
+
from memory, and do not treat its silence as a design review.
|
|
257
|
+
|
|
258
|
+
What it cannot see is the other half: fabricated substance (a product UI built from divs,
|
|
259
|
+
invented photo credits, the Jane Doe cast), decoration posing as information, labels that say
|
|
260
|
+
nothing, and composition habits like three identical feature cards. That half is
|
|
261
|
+
`references/ai-tells.md`, and it is read with your eyes on the page.
|
|
262
|
+
|
|
263
|
+
When the brief corresponds to an official, maintained design system rather than an aesthetic —
|
|
264
|
+
UK or US public sector, Shopify admin, Carbon, Atlassian, Primer, Fluent — install the
|
|
265
|
+
official package instead of approximating it: `references/design-systems.md`.
|
|
266
|
+
|
|
237
267
|
## Anti-patterns
|
|
238
268
|
|
|
239
269
|
| Rationalization | Reality / Fix |
|
|
@@ -314,6 +344,6 @@ Score = Σ(dimension × weight), max 115 (normalize to /100 by ×100/115 if you
|
|
|
314
344
|
Two records, both indexed in `02-DOCS/wiki/index.md` (the Knowledge map; root `CLAUDE.md` keeps only a short pointer to it), both read first on every use so outputs stay consistent with them:
|
|
315
345
|
|
|
316
346
|
- The **brand study** at `02-DOCS/wiki/brand/` — the hard gate above: missing or incomplete → ask until complete before designing.
|
|
317
|
-
- The **design-system decisions** at `02-DOCS/wiki/stack/design.md` — the chosen tokens (color/OKLCH, type scale, spacing, radius, shadow, motion), the 2026 direction picked, and the reference sites. Recorded, not gated; create/update it as decisions are made.
|
|
347
|
+
- The **design-system decisions** at `02-DOCS/wiki/stack/design.md` — the chosen tokens (color/OKLCH, type scale, spacing, radius, shadow, motion), the 2026 direction picked, and the reference sites. Recorded, not gated; create/update it as decisions are made. **Read it before fixing type and palette on a new surface**, and either reuse what is there or state why you are departing — otherwise every surface of the project drifts into a different design. (Across *different* projects this record cannot help: it lives inside each one. Distrusting your own second default is the only guard there — see the repetition note in `references/ai-tells.md`.)
|
|
318
348
|
|
|
319
349
|
The wiki article protocol both follow is `../harness/SKILL.md`'s.
|
|
@@ -49,7 +49,7 @@ capability:
|
|
|
49
49
|
- scenario: "User asks: 'Here's our current pricing page — do a design review and tell me how good it actually is.'"
|
|
50
50
|
must_include:
|
|
51
51
|
- "Applies the design-review QA checklist (value prop in 5s, contrast >= 4.5:1, visible focus, one <h1>, semantic landmarks, LCP image priority, next/font, no transition: all, tokens not magic numbers, ban-list-clean copy, 360px fit, empty/loading/error states)."
|
|
52
|
-
- "Scores the
|
|
52
|
+
- "Scores the 11-dimension visual-audit rubric 0-10 with weights and reports the weighted total on its /115 scale, mapped to a band (<69 generic / 69-91 competent / 92-108 premium / 109+ award-tier)."
|
|
53
53
|
- "For each dimension scoring below 8, gives a concrete actionable fix (e.g. exact spacing/token change), not a vague 'improve spacing'."
|
|
54
54
|
- "Checks copy against the ban-list and flags hype/vague value props that fail the 5s test."
|
|
55
55
|
- "Flags any transition: all, magic hex/px, cards-in-cards, or purple->blue everything gradient as defects, not preferences."
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# AI tells — the ones only a reader can catch
|
|
2
|
+
|
|
3
|
+
The mechanical tells are **not here**. `scripts/verify.sh` carries a registry of them and
|
|
4
|
+
flags them with file and line, so re-teaching them as prose would be paying twice for the
|
|
5
|
+
same rule. This file holds only what a `grep` cannot decide: presences you have to *look* at
|
|
6
|
+
the page to see.
|
|
7
|
+
|
|
8
|
+
Part of this corpus is adapted from [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill)
|
|
9
|
+
(MIT), whose lists came out of real production tests. The split into checkable-vs-judged, the
|
|
10
|
+
thresholds, and the wording are ours.
|
|
11
|
+
|
|
12
|
+
Every item below is a **default**, overridable by a brief that explicitly asks for it. What
|
|
13
|
+
makes them tells is that they show up *unasked*, as the model's idea of "looking designed".
|
|
14
|
+
|
|
15
|
+
## Fabricated substance
|
|
16
|
+
|
|
17
|
+
- **A product UI built out of divs.** A fake task list, fake terminal or fake dashboard
|
|
18
|
+
assembled from styled rectangles is the single most reliable signature of generated work.
|
|
19
|
+
Use a real screenshot, a real component, a generated image, or show nothing.
|
|
20
|
+
- **Hand-rolled decorative SVG.** Abstract squiggles and blobs authored inline to fill space.
|
|
21
|
+
Icons come from a maintained set; decoration comes from a real asset or not at all.
|
|
22
|
+
- **The Jane Doe cast.** "John Doe", "Sarah Chan", egg avatars, `99.99%`, `50%`, round
|
|
23
|
+
thousands, and startup-slop brand names ("Acme", "Nexus", "SmartFlow"). Real work has
|
|
24
|
+
specific, slightly awkward names and messy numbers (`47.2%`, not `50%`).
|
|
25
|
+
- **Testimonials nobody said.** A quote with no attributable person, or five quotes with the
|
|
26
|
+
same cadence, reads as filler. One real quote beats three invented ones.
|
|
27
|
+
|
|
28
|
+
## Decoration wearing the costume of information
|
|
29
|
+
|
|
30
|
+
- **Atmospheric locale strips.** A city, a timezone, a temperature in the nav or footer. Only
|
|
31
|
+
earns its place for a genuinely distributed studio, a travel brand, or a physical venue.
|
|
32
|
+
- **Status dots with no status.** A coloured dot before nav items, list rows or badges. Keep
|
|
33
|
+
it only where it reports real state, and then only once per section.
|
|
34
|
+
- **Invented photo credits.** `Field study no. 12 · Ines Caetano` under a stock image.
|
|
35
|
+
Credit a real photographer or write a functional caption.
|
|
36
|
+
- **Pills laid over images.** Tags floated on top of a photo. Either the image speaks alone,
|
|
37
|
+
or the caption sits below it, outside the frame.
|
|
38
|
+
- **Decoration strips under the hero.** `BRAND. MOTION. SPATIAL.` in small mono caps. Only
|
|
39
|
+
legitimate when those words are real navigation or real status.
|
|
40
|
+
|
|
41
|
+
## Labels that say nothing
|
|
42
|
+
|
|
43
|
+
- **Poetic section labels.** "Field notes", "From the field", "On our desks", "Currently on
|
|
44
|
+
the bench". Use the functional name ("Testimonials", "Latest writing") or no label.
|
|
45
|
+
- **Generic step labels.** "Stage 1 / Stage 2", "Phase 01 / 02". The step content *is* the
|
|
46
|
+
label: "Install", "Configure", "Ship".
|
|
47
|
+
- **Mock-humble asides** and micro-meta sentences under a heading ("The list will stay short
|
|
48
|
+
on purpose"). Eyebrow, headline, body. Nothing else.
|
|
49
|
+
- **"Quietly trusted by".** Say "Used at", "Customers include", or let the logos speak.
|
|
50
|
+
|
|
51
|
+
## Composition habits
|
|
52
|
+
|
|
53
|
+
- **Three identical feature cards.** The default feature row. Reach for a two-column zig-zag,
|
|
54
|
+
an asymmetric grid, or a horizontal rail instead — and do not run the same split three
|
|
55
|
+
sections in a row.
|
|
56
|
+
- **The corner floater.** A giant left-aligned headline with a small explainer paragraph
|
|
57
|
+
floating in the section's top-right, aligned to nothing. Stack it, or build a real
|
|
58
|
+
two-column header.
|
|
59
|
+
- **Rotated vertical text** and crosshair grid lines drawn only to look designed. Both are
|
|
60
|
+
agency-portfolio clichés unless they organise real content.
|
|
61
|
+
- **A bento with empty cells.** N items means N cells, and at least two cells need real
|
|
62
|
+
visual variation — otherwise it is a white card grid with extra steps.
|
|
63
|
+
|
|
64
|
+
## The second default (repetition)
|
|
65
|
+
|
|
66
|
+
Obeying every rule produces correct pages that all look the same. Before fixing type and
|
|
67
|
+
palette, read the project's recorded design decisions and either reuse them deliberately or
|
|
68
|
+
say why you are departing. Two specific defaults to distrust: reaching for the same display
|
|
69
|
+
serif again, and answering every premium-consumer brief with the beige/brass/oxblood family.
|
|
70
|
+
|
|
71
|
+
## Surface treatments to distrust by default
|
|
72
|
+
|
|
73
|
+
Outer glows and neon; pure `#000000` (use an off-black); oversaturated accents that refuse to
|
|
74
|
+
sit with the neutrals; gradient fills on large headings; custom cursors (outdated, hostile to
|
|
75
|
+
accessibility and to performance); glass on every surface rather than on floating ones.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# When the brief is a design system, not an aesthetic
|
|
2
|
+
|
|
3
|
+
This skill's default is Tailwind v4 + Next.js, and for most briefs that is right. But some
|
|
4
|
+
briefs correspond to a **maintained, official design system**, and there the correct answer is
|
|
5
|
+
to install it. Recreating its CSS by hand produces something that looks like it, fails its
|
|
6
|
+
accessibility work, and drifts on its next release.
|
|
7
|
+
|
|
8
|
+
The list is deliberately short: only the cases where using the official package is
|
|
9
|
+
**obligatory or strongly expected**, and where the default instinct would be to hand-roll it.
|
|
10
|
+
Free choices (Bootstrap, Material for a non-Google product, Radix Themes, shadcn/ui) are not
|
|
11
|
+
here — they are ordinary picks, and shadcn/ui in particular is already this skill's territory,
|
|
12
|
+
where the rule is simply never to ship it in its default state.
|
|
13
|
+
|
|
14
|
+
| The brief reads as | Install | Why it is not a choice |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| A UK public-sector service | `govuk-frontend` | Expected by the Service Standard; deviating is a finding at assessment |
|
|
17
|
+
| A US federal or trust-first public service | `uswds` | Same, under the US Web Design System |
|
|
18
|
+
| A Shopify admin surface or embedded app | `@shopify/polaris` (or Polaris web components) | Required for admin UI to be accepted |
|
|
19
|
+
| IBM-flavoured enterprise or dense analytics | `@carbon/react` + `@carbon/styles` | Mature data-density patterns you will otherwise reinvent badly |
|
|
20
|
+
| An Atlassian / Jira-adjacent product surface | `@atlaskit/*` + `@atlaskit/tokens` | Official DS; token-driven theming |
|
|
21
|
+
| A GitHub-style devtool or community page | `@primer/css`, or `@primer/react-brand` for marketing | Official Primer; the Brand variant is the marketing half |
|
|
22
|
+
| A Microsoft / Fluent-flavoured product | `@fluentui/react-components` | Official Fluent, Microsoft tokens, accessibility already done |
|
|
23
|
+
|
|
24
|
+
## The three rules that come with them
|
|
25
|
+
|
|
26
|
+
1. **Do not recreate it.** If the brief names one of these, install the package. Importing its
|
|
27
|
+
tokens and then overriding ninety percent of them is the same mistake with extra steps.
|
|
28
|
+
2. **One system per project.** No Fluent components inside a Carbon tree, no shadcn/ui
|
|
29
|
+
dropped into a Material app. Mixing two systems produces a third, worse one.
|
|
30
|
+
3. **Name what is an approximation.** When the brief is an *aesthetic* rather than a system —
|
|
31
|
+
glassmorphism, bento tiles, brutalism, editorial, aurora gradients, kinetic type — there is
|
|
32
|
+
no official package, and saying so is part of the work. Apple's Liquid Glass is documented
|
|
33
|
+
for Apple platforms only: there is no official web implementation, so a web version is a
|
|
34
|
+
`backdrop-filter` approximation and must be labelled as one, in the code and to the user.
|
|
35
|
+
|
|
36
|
+
## What still applies
|
|
37
|
+
|
|
38
|
+
Everything in `../SKILL.md` that is not styling: the brand gate, the accessibility floor,
|
|
39
|
+
the Core Web Vitals budget, the 5-second value-prop test, and the tell registry in
|
|
40
|
+
`../scripts/verify.sh`. An official design system settles which components you use. It does
|
|
41
|
+
not settle whether the page says anything.
|
|
@@ -174,6 +174,41 @@ export function Toast({ open, message }: { open: boolean; message: string }) {
|
|
|
174
174
|
|
|
175
175
|
Once you are in `motion/react`, defer the mechanics (spring config, variants, `layoutId` shared transitions) to the external *motion-foundations* and *motion-ui* references.
|
|
176
176
|
|
|
177
|
+
## Escalating past the native path
|
|
178
|
+
|
|
179
|
+
Native `animation-timeline: view()` covers reveals, progress rails and parallax. Two effects it
|
|
180
|
+
cannot express are **pinning** (a section holds still while its content advances) and a
|
|
181
|
+
**horizontal pan** driven by vertical scroll. Those need a scroll library, and the improvised
|
|
182
|
+
version is always wrong in the same three ways. Escalate deliberately:
|
|
183
|
+
|
|
184
|
+
**Never listen to scroll directly.** `window.addEventListener('scroll')` fires off the main
|
|
185
|
+
thread's budget, blows INP, and never gets a passive-listener or cleanup story right. The
|
|
186
|
+
allowed inputs are `IntersectionObserver`, a scroll-driven CSS timeline, `useScroll()` from
|
|
187
|
+
`motion/react`, or GSAP's ScrollTrigger. Nothing else.
|
|
188
|
+
|
|
189
|
+
**Sticky stack** (pin a section, advance its layers):
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// 'use client' leaf only. One trigger per pinned section, killed on unmount.
|
|
193
|
+
const st = ScrollTrigger.create({
|
|
194
|
+
trigger: sectionRef.current,
|
|
195
|
+
start: "top top", // pin the moment the section's top reaches the viewport top
|
|
196
|
+
end: () => `+=${panels.length * window.innerHeight}`,
|
|
197
|
+
pin: true,
|
|
198
|
+
pinSpacing: true, // without this the next section slides under the pin
|
|
199
|
+
scrub: 1, // 1 = follow scroll with a 1s catch-up, not a hard 1:1 jump
|
|
200
|
+
invalidateOnRefresh: true, // recompute on resize, or the end point is stale on rotate
|
|
201
|
+
});
|
|
202
|
+
// cleanup is not optional: return () => st.kill();
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Horizontal pan** is the same trigger with `end: () => "+=" + track.scrollWidth` and the
|
|
206
|
+
track moved via `x`, not `left` — `transform` is compositor-only, `left` triggers layout.
|
|
207
|
+
|
|
208
|
+
Both are opt-in above a motion budget, both stay inside
|
|
209
|
+
`@media (prefers-reduced-motion: no-preference)`, and both collapse to a plain vertical stack
|
|
210
|
+
below `768px` rather than pinning on a phone.
|
|
211
|
+
|
|
177
212
|
## See Also
|
|
178
213
|
|
|
179
214
|
- `visual-system.md` — the easing and duration tokens these animations consume.
|
|
@@ -7,9 +7,17 @@
|
|
|
7
7
|
# performance + accessibility audit and checks Core Web Vitals against the
|
|
8
8
|
# skill's hard thresholds (LCP < 2.5s, CLS < 0.1, INP proxy via TBT < 200ms,
|
|
9
9
|
# a11y score >= 0.9).
|
|
10
|
-
# 2. Always runs static, network-free design-review
|
|
11
|
-
#
|
|
12
|
-
#
|
|
10
|
+
# 2. Always runs static, network-free design-review checks, in three groups:
|
|
11
|
+
# - four judgement-shaped ones (more than one <h1>, image without alt,
|
|
12
|
+
# hardcoded hex despite a token system, animation with no reduced-motion
|
|
13
|
+
# guard) — see static_checks();
|
|
14
|
+
# - the tell registry: one row per checkable AI tell, iterated generically
|
|
15
|
+
# (dashes in rendered copy, scroll cues, numbered eyebrows, build stamps,
|
|
16
|
+
# unstable viewport heights, middot chains, transition-all, hype words)
|
|
17
|
+
# — see tell_table(); every row has a fixture in the repo's test suite;
|
|
18
|
+
# - three counters that need a ratio rather than a match (eyebrows against
|
|
19
|
+
# sections, double-bordered rows, radius scales) — see counter_checks().
|
|
20
|
+
# Everything here WARNS. Nothing here fails, unless --strict is passed.
|
|
13
21
|
# 3. If Lighthouse did not run, prints the manual QA checklist. For a graded
|
|
14
22
|
# 0-10 critique, use the visual-audit rubric in SKILL.md instead.
|
|
15
23
|
#
|
|
@@ -78,7 +86,10 @@ have() { command -v "$1" >/dev/null 2>&1; }
|
|
|
78
86
|
SELF_NAME="$(basename "$0")"
|
|
79
87
|
search() {
|
|
80
88
|
[ "$#" -gt 0 ] || return 1
|
|
81
|
-
|
|
89
|
+
# VERIFY_FORCE_GREP=1 pins the grep branch. It exists so the test suite can prove BOTH
|
|
90
|
+
# engines agree on the multibyte patterns (em-dash, middot) on any machine, instead of
|
|
91
|
+
# only exercising whichever one happens to be installed.
|
|
92
|
+
if have rg && [ -z "${VERIFY_FORCE_GREP:-}" ]; then
|
|
82
93
|
# rg already ignores .gitignore'd paths (node_modules, .next, dist); also skip self.
|
|
83
94
|
rg -n --no-heading --glob "!$SELF_NAME" "$@"
|
|
84
95
|
else
|
|
@@ -165,6 +176,102 @@ run_lighthouse() {
|
|
|
165
176
|
fi
|
|
166
177
|
}
|
|
167
178
|
|
|
179
|
+
# --- the tell registry ------------------------------------------------------
|
|
180
|
+
# One row per checkable AI tell. A row is DATA, not code, so the test suite can enumerate
|
|
181
|
+
# every row and demand a fixture for each: a check nobody has ever seen fire cannot ship.
|
|
182
|
+
# Add a tell by adding a row plus tests/fixtures/design-tells/tells/<id>.tsx — nothing else.
|
|
183
|
+
#
|
|
184
|
+
# Columns, delimited by %% (the pattern column needs `|` for ERE alternation):
|
|
185
|
+
# id %% sev %% extensions %% POSIX-ERE pattern %% what to do instead / the legit exception
|
|
186
|
+
#
|
|
187
|
+
# Patterns are POSIX ERE: no lookahead, no \b, no \s — they must behave identically under rg
|
|
188
|
+
# and under grep -E. Part of the corpus is adapted from Leonxlnx/taste-skill (MIT).
|
|
189
|
+
tell_table() {
|
|
190
|
+
cat <<'TELLS'
|
|
191
|
+
em-dash%%warn%%tsx,jsx,html,vue,svelte,mdx%%—|–%%em-dash or en-dash in rendered copy is the loudest AI tell. Use a period, a comma, a colon or parentheses instead
|
|
192
|
+
scroll-cue%%warn%%tsx,jsx,html,vue,svelte%%[Ss]croll to (explore|walk|discover|see|begin)|↓ *[Ss]croll%%a scroll cue explains scrolling to someone already looking at the hero. Delete it, or use a real anchor link instead
|
|
193
|
+
numbered-eyebrow%%warn%%tsx,jsx,html,vue,svelte%%(^|[^0-9A-Za-z])[0-9]{2,3} +(·|/) +[A-Za-z]%%numbered section eyebrow (001 · Capabilities). Name the topic in plain language instead
|
|
194
|
+
version-stamp%%warn%%tsx,jsx,html,vue,svelte%%v[0-9]+\.[0-9]+\.[0-9]+|Build [0-9]{3,4}%%a build or version stamp is devtool furniture on a marketing page. Delete it. Legitimate exception: a real devtool footer, a changelog or a release page
|
|
195
|
+
viewport-unit%%warn%%tsx,jsx,html,vue,svelte,css%%h-screen%%h-screen jumps when mobile browser chrome slides away. Use min-h-[100dvh] instead
|
|
196
|
+
middot-chain%%warn%%tsx,jsx,html,vue,svelte%%·[^·]*·%%more than one middot on a line is decoration pretending to be metadata. Use line breaks, columns or a hairline instead
|
|
197
|
+
transition-all%%warn%%tsx,jsx,css,html,vue,svelte%%transition:[[:space:]]*all|transition-all%%transition-all animates layout properties and janks. List the exact properties instead
|
|
198
|
+
ban-list%%warn%%tsx,jsx,html,md,mdx%%revolutionary|game-?changer|cutting-edge|supercharge|seamless|unlock%%hype words carry no value prop. Use the concrete benefit and a real number instead
|
|
199
|
+
TELLS
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
run_tell_table() {
|
|
203
|
+
local line id sev globs pat msg hits exts rest
|
|
204
|
+
# NOT `IFS='%%'`: IFS is a set of characters, so it would split on a single `%` and put an
|
|
205
|
+
# empty field between every pair. Parameter expansion splits on the two-character
|
|
206
|
+
# delimiter exactly, which is what the pattern column needs to keep its ERE `|`.
|
|
207
|
+
while IFS= read -r line; do
|
|
208
|
+
case "$line" in ''|'#'*) continue ;; esac
|
|
209
|
+
id="${line%%"%%"*}"; rest="${line#*"%%"}"
|
|
210
|
+
sev="${rest%%"%%"*}"; rest="${rest#*"%%"}"
|
|
211
|
+
globs="${rest%%"%%"*}"; rest="${rest#*"%%"}"
|
|
212
|
+
pat="${rest%%"%%"*}"; msg="${rest#*"%%"}"
|
|
213
|
+
exts="${globs//,/|}"
|
|
214
|
+
# Two post-filters, both learned from a real run over this repo:
|
|
215
|
+
# 1. keep only the extensions the row declares (anchored before the first colon, so it
|
|
216
|
+
# behaves the same under rg and grep and never matches content that names a file);
|
|
217
|
+
# 2. drop lines whose match sits in a code comment. A dash in `/* tokens — lifted */`
|
|
218
|
+
# is not rendered copy, and flagging it trains the reader to ignore the tool.
|
|
219
|
+
hits="$(search "$pat" 2>/dev/null \
|
|
220
|
+
| grep -E "^[^:]*\.($exts):" \
|
|
221
|
+
| grep -vE "^[^:]*:[0-9]+:[[:space:]]*(//|/\*|\*[^a-zA-Z]|#|<!--|\{/\*)" || true)"
|
|
222
|
+
if [ -n "$hits" ]; then
|
|
223
|
+
# The sev column is real, not decoration: anything other than `warn` breaks the build.
|
|
224
|
+
case "$sev" in
|
|
225
|
+
warn) warn "$id: $msg" ;;
|
|
226
|
+
*) fail "$id: $msg" ;;
|
|
227
|
+
esac
|
|
228
|
+
printf '%s\n' "$hits" | head -n 5
|
|
229
|
+
fi
|
|
230
|
+
done <<EOF
|
|
231
|
+
$(tell_table)
|
|
232
|
+
EOF
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# --- counters: the tells that are a ratio or a repetition, not a pattern -----
|
|
236
|
+
# These cannot be table rows because one match proves nothing — it takes a count. Each one
|
|
237
|
+
# carries its own named test instead of riding the generic row mechanism.
|
|
238
|
+
counter_checks() {
|
|
239
|
+
local eyebrows sections ceiling hits variants v
|
|
240
|
+
|
|
241
|
+
# Eyebrow ceiling. Threshold ceil(sections/3) is BORROWED from taste-skill and has never
|
|
242
|
+
# been calibrated against a page of ours — the warning says so rather than implying rigour.
|
|
243
|
+
eyebrows="$(search 'uppercase[^"]*tracking|tracking[^"]*uppercase' 2>/dev/null | grep -cE '^[^:]*\.(tsx|jsx|html|vue|svelte):' || true)"
|
|
244
|
+
sections="$(search '<section' 2>/dev/null | grep -cE '^[^:]*\.(tsx|jsx|html|vue|svelte):' || true)"
|
|
245
|
+
[ -z "$eyebrows" ] && eyebrows=0
|
|
246
|
+
[ -z "$sections" ] && sections=0
|
|
247
|
+
if [ "$sections" -eq 0 ]; then
|
|
248
|
+
if [ "$eyebrows" -gt 0 ]; then
|
|
249
|
+
skip "eyebrow ceiling: no <section> found, so there is no denominator — not guessing one"
|
|
250
|
+
fi
|
|
251
|
+
else
|
|
252
|
+
ceiling=$(( (sections + 2) / 3 ))
|
|
253
|
+
if [ "$eyebrows" -gt "$ceiling" ]; then
|
|
254
|
+
warn "eyebrow count $eyebrows exceeds the ceiling $ceiling for $sections sections (uncalibrated threshold, borrowed): drop the uppercase micro-labels, or let the headline carry it instead"
|
|
255
|
+
fi
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
# A hairline above AND below every row of a long list is the laziest possible layout.
|
|
259
|
+
hits="$(search 'border-t[^"]*border-b|border-b[^"]*border-t' 2>/dev/null | grep -cE '^[^:]*\.(tsx|jsx|html|vue|svelte):' || true)"
|
|
260
|
+
[ -z "$hits" ] && hits=0
|
|
261
|
+
if [ "$hits" -gt 3 ]; then
|
|
262
|
+
warn "double border on $hits list rows: pick one edge, or use spacing instead of hairlines"
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
# More than two radius scales in one tree means no radius system at all.
|
|
266
|
+
variants=0
|
|
267
|
+
for v in none sm md lg xl 2xl 3xl full; do
|
|
268
|
+
if search "rounded-$v" >/dev/null 2>&1; then variants=$((variants + 1)); fi
|
|
269
|
+
done
|
|
270
|
+
if [ "$variants" -gt 2 ]; then
|
|
271
|
+
warn "$variants different radius scales in use: one radius system per project — use a --radius-* token instead"
|
|
272
|
+
fi
|
|
273
|
+
}
|
|
274
|
+
|
|
168
275
|
# --- static design-review checks (always run, no network) -------------------
|
|
169
276
|
static_checks() {
|
|
170
277
|
local hits
|
|
@@ -182,9 +289,8 @@ static_checks() {
|
|
|
182
289
|
done < <(grep -rl --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=.next '<h1' . 2>/dev/null || true)
|
|
183
290
|
fi
|
|
184
291
|
|
|
185
|
-
# 2. transition: all
|
|
186
|
-
|
|
187
|
-
if [ -n "$hits" ]; then warn "transition: all / transition-all found:"; printf '%s\n' "$hits" | head -n 5; fi
|
|
292
|
+
# 2. (moved) `transition: all` is now a row in tell_table() — it is a plain pattern, so it
|
|
293
|
+
# gets the generic mechanism and its fixture for free.
|
|
188
294
|
|
|
189
295
|
# 3. hardcoded hex when a token system exists (\b is not portable in ERE; a
|
|
190
296
|
# 3-8 char hex run after # is a sufficient heuristic without the boundary).
|
|
@@ -209,9 +315,7 @@ static_checks() {
|
|
|
209
315
|
fi
|
|
210
316
|
fi
|
|
211
317
|
|
|
212
|
-
# 6. ban-list
|
|
213
|
-
hits="$(search 'revolutionary|game-?changer|cutting-edge|supercharge|seamless|unlock' 2>/dev/null | grep -iE '\.(tsx|jsx|html|md|mdx)' || true)"
|
|
214
|
-
if [ -n "$hits" ]; then warn "ban-list marketing words found in copy:"; printf '%s\n' "$hits" | head -n 5; fi
|
|
318
|
+
# 6. (moved) the marketing ban-list is now a row in tell_table(), same reason as #2.
|
|
215
319
|
}
|
|
216
320
|
|
|
217
321
|
# --- fallback manual checklist ----------------------------------------------
|
|
@@ -239,6 +343,8 @@ EOF
|
|
|
239
343
|
# --- run --------------------------------------------------------------------
|
|
240
344
|
run_lighthouse
|
|
241
345
|
static_checks
|
|
346
|
+
run_tell_table
|
|
347
|
+
counter_checks
|
|
242
348
|
[ "$LH_RAN" -eq 0 ] && print_checklist
|
|
243
349
|
|
|
244
350
|
printf '\nok=%d skip=%d warn=%d fail=%d\n' "$ok_count" "$skip_count" "$warn_count" "$fail_count"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: git-workflow
|
|
3
|
-
description: "Use when naming or scoping a branch, writing or fixing a commit message, untangling history (rebase versus merge versus squash), or cutting a versioned release — the portable git-convention layer for any repo. Covers Conventional Commits, SemVer tags, branch hygiene, force-push safety and gh pr/release mechanics. NOT the land-it decision and pre-ship checklist (that is `ship`), NOT an isolated checkout before coding (that is `worktrees`), NOT CI/CD release automation (that is `deployment`)."
|
|
4
|
-
tags: [git, version-control, conventional-commits, semver, pull-request]
|
|
3
|
+
description: "Use when naming or scoping a branch, writing or fixing a commit message, picking the gitmoji for a commit, untangling history (rebase versus merge versus squash), or cutting a versioned release — the portable git-convention layer for any repo. Covers gitmoji + Conventional Commits, SemVer tags, branch hygiene, force-push safety and gh pr/release mechanics. NOT the land-it decision and pre-ship checklist (that is `ship`), NOT an isolated checkout before coding (that is `worktrees`), NOT CI/CD release automation (that is `deployment`)."
|
|
4
|
+
tags: [git, version-control, conventional-commits, gitmoji, semver, pull-request]
|
|
5
5
|
recommends: [ship, worktrees, deployment, github-actions]
|
|
6
6
|
origin: risco
|
|
7
7
|
---
|
|
@@ -44,12 +44,12 @@ Good fix/expired-refresh-token-401
|
|
|
44
44
|
Good chore/bump-node-22
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
## Conventional Commits
|
|
47
|
+
## Commit grammar — gitmoji + Conventional Commits
|
|
48
48
|
|
|
49
|
-
Write every commit to **Conventional Commits 1.0.0**. The structure:
|
|
49
|
+
Write every commit to **Conventional Commits 1.0.0**, opened by a **gitmoji**. The structure:
|
|
50
50
|
|
|
51
51
|
```text
|
|
52
|
-
type(scope)!: subject
|
|
52
|
+
<gitmoji> type(scope)!: subject
|
|
53
53
|
|
|
54
54
|
body — what changed and why, wrapped, optional
|
|
55
55
|
|
|
@@ -57,6 +57,8 @@ BREAKING CHANGE: description of the incompatible change
|
|
|
57
57
|
Fixes #123
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
- The **gitmoji is mandatory** and comes first — the intention of the change, readable in one glyph
|
|
61
|
+
when you scan `git log --oneline`. `type` is what tooling reads; the emoji is what humans read.
|
|
60
62
|
- `type` is mandatory. `scope` in parentheses is optional. `!` before the colon marks a breaking change.
|
|
61
63
|
- Subject: imperative mood ("add", not "added"/"adds"), ≤72 chars, no trailing period.
|
|
62
64
|
- Body explains *why*, not *what the diff already shows*. Separate from subject by a blank line.
|
|
@@ -74,23 +76,44 @@ Type → SemVer effect:
|
|
|
74
76
|
`BREAKING CHANGE` **must be uppercase** in the footer; the type/scope units are case-insensitive but
|
|
75
77
|
write them lowercase by convention.
|
|
76
78
|
|
|
79
|
+
Type → gitmoji, the everyday set (the full 75-emoji table, and *why* the emoji never replaces the
|
|
80
|
+
type, are in **`references/gitmoji.md`**):
|
|
81
|
+
|
|
82
|
+
| Type | gitmoji | Type | gitmoji | Type | gitmoji |
|
|
83
|
+
|------------|---------|------------|---------|-----------|---------|
|
|
84
|
+
| `feat` | ✨ | `refactor` | ♻️ | `build` | 📦️ |
|
|
85
|
+
| `fix` | 🐛 | `test` | ✅ | `style` | 🎨 |
|
|
86
|
+
| `docs` | 📝 | `perf` | ⚡️ | `revert` | ⏪️ |
|
|
87
|
+
| `chore` | 🔧 | `ci` | 👷 | breaking | 💥 |
|
|
88
|
+
|
|
89
|
+
Pick by **intention**, not by which file changed, and prefer the specific one: 🚑️ for a production
|
|
90
|
+
hotfix, 🩹 for a trivial non-critical fix, 🔥 for a deletion, 🚚 for a rename, ⬆️ for a dep bump,
|
|
91
|
+
🔖 for a release commit.
|
|
92
|
+
|
|
77
93
|
```text
|
|
78
94
|
Bad fix stuff
|
|
79
95
|
Bad updates
|
|
80
96
|
Bad Fixed the login bug. (past tense, capitalized, trailing period)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Good
|
|
97
|
+
Bad fix(auth): reject expired refresh tokens (no gitmoji)
|
|
98
|
+
Bad ✨ added a search endpoint (gitmoji but no type → no derivable bump)
|
|
99
|
+
Good 🐛 fix(auth): reject expired refresh tokens
|
|
100
|
+
Good ✨ feat(api): add /v2/search endpoint with cursor paging
|
|
101
|
+
Good ♻️ refactor(parser): extract token scanner, no behavior change
|
|
84
102
|
```
|
|
85
103
|
|
|
86
104
|
A breaking change, both forms equivalent:
|
|
87
105
|
|
|
88
106
|
```text
|
|
89
|
-
feat(api)!: drop the legacy /v1 search endpoint
|
|
107
|
+
💥 feat(api)!: drop the legacy /v1 search endpoint
|
|
90
108
|
|
|
91
109
|
BREAKING CHANGE: /v1/search is removed; callers must migrate to /v2/search.
|
|
92
110
|
```
|
|
93
111
|
|
|
112
|
+
If the repo runs a **strict** conventional parser (commitlint, semantic-release) it anchors the type
|
|
113
|
+
at position 0 and rejects the emoji prefix. Either widen its `headerPattern` — the config is in
|
|
114
|
+
`references/gitmoji.md` — or move the emoji behind the header (`feat(api): ✨ add cursor paging`),
|
|
115
|
+
which every parser accepts. Both forms satisfy this convention; dropping the gitmoji does not.
|
|
116
|
+
|
|
94
117
|
**Authorship is always Eric.** Never add a `Co-Authored-By: Claude` trailer, never a
|
|
95
118
|
"Generated with" footer, never any line crediting an AI tool — in a commit *or* a PR body. The work
|
|
96
119
|
is Eric's; the agent is a tool, like the compiler.
|
|
@@ -103,7 +126,7 @@ Decide by who else has the commits. The lease rule below is non-negotiable.
|
|
|
103
126
|
|------------------------------------------------------|------------------------------------------------------|------------------------------------------------------------|
|
|
104
127
|
| Private branch, only you have it, want linear history | `git rebase main`, then `git push --force-with-lease`| rebase rewrites hashes; safe because nobody built on them |
|
|
105
128
|
| Branch others have pulled / built on | `git merge main` — **never** rebase it | rebase changes every hash; collaborators' work diverges |
|
|
106
|
-
| Noisy PR (many `wip` commits) | squash-merge into one conventional commit | `main` gets one meaningful entry, not 9 scratch commits |
|
|
129
|
+
| Noisy PR (many `wip` commits) | squash-merge into one gitmoji + conventional commit | `main` gets one meaningful entry, not 9 scratch commits |
|
|
107
130
|
| Already pushed, shared, *and* you rewrote it | **STOP** — coordinate, or `git revert` instead | force-pushing shared history breaks everyone downstream |
|
|
108
131
|
|
|
109
132
|
After a rebase, push with `--force-with-lease`, never bare `--force`:
|
|
@@ -168,7 +191,8 @@ enough to hand over. Setting up the isolated checkout *before* you start coding
|
|
|
168
191
|
| Anti-pattern | Why it hurts | Instead |
|
|
169
192
|
|-------------------------------------------------------|-----------------------------------------------------------|------------------------------------------------------|
|
|
170
193
|
| `git push --force` on a shared branch | silently erases teammates' commits | `--force-with-lease`, or don't rewrite shared history |
|
|
171
|
-
| `git commit -m "wip"` / `"fix"` / `"updates"` | the log carries zero signal for the next reader |
|
|
194
|
+
| `git commit -m "wip"` / `"fix"` / `"updates"` | the log carries zero signal for the next reader | `<gitmoji> type(scope): imperative subject` |
|
|
195
|
+
| Commit message with no gitmoji | `git log --oneline` reads as a wall of undifferentiated text | pick the intention's emoji (`references/gitmoji.md`) |
|
|
172
196
|
| Mixing unrelated changes in one commit | can't revert or review one concern in isolation | one logical change per commit |
|
|
173
197
|
| Long-lived branch (weeks) | diverges from `main`, merge becomes a battle | short-lived; rebase or merge `main` in often |
|
|
174
198
|
| Hand-computing the semver bump | breaking change shipped as a MINOR → broken downstream | derive the bump from the commit log |
|
|
@@ -183,7 +207,7 @@ enough to hand over. Setting up the isolated checkout *before* you start coding
|
|
|
183
207
|
|
|
184
208
|
- [ ] Working tree clean (`git status`), no stray or generated files staged.
|
|
185
209
|
- [ ] Branch rebased on / merged with current `main`; no avoidable conflicts.
|
|
186
|
-
- [ ] Every commit
|
|
210
|
+
- [ ] Every commit carries its gitmoji **and** a conventional header; `wip`/scratch commits squashed away.
|
|
187
211
|
- [ ] No secrets, no AI-attribution trailers.
|
|
188
212
|
- [ ] PR body explains the why and links the issue (`Fixes #`).
|
|
189
213
|
- [ ] (Release) version bump **derived from the commit log**, tag is `vX.Y.Z`, annotated.
|
|
@@ -13,6 +13,10 @@ should_trigger:
|
|
|
13
13
|
why: Spanish trigger covering history cleanup plus conventional-commit formatting.
|
|
14
14
|
- prompt: "Name a branch for the OAuth PKCE work"
|
|
15
15
|
why: Branch-naming convention — feat/ prefix plus kebab slug from intent.
|
|
16
|
+
- prompt: "Which gitmoji goes on this commit — I deleted a deprecated module"
|
|
17
|
+
why: Gitmoji selection by intention (🔥 remove code, not ♻️ refactor); the commit-grammar layer owns the emoji.
|
|
18
|
+
- prompt: "Mi commitlint rechaza el commit por el emoji delante del type"
|
|
19
|
+
why: Spanish; the strict-parser escape hatch (widen headerPattern or move the emoji behind the header) lives in this skill.
|
|
16
20
|
- prompt: "Tag v2.1.0 and write the release notes"
|
|
17
21
|
why: Release mechanics — annotated tag plus gh release create --generate-notes.
|
|
18
22
|
|
|
@@ -41,6 +45,7 @@ capability:
|
|
|
41
45
|
must_include:
|
|
42
46
|
- Collapses the two "wip" commits via squash or fixup and explains why (one meaningful entry in main).
|
|
43
47
|
- Identifies a feat commit and a BREAKING CHANGE footer written in uppercase.
|
|
48
|
+
- Every proposed commit message opens with a gitmoji (✨ for the feature, 💥 for the breaking removal) before the conventional header.
|
|
44
49
|
- Bump is MAJOR because of the breaking change, with a tag of the form vX.0.0.
|
|
45
50
|
- Uses gh release create vX.0.0 --generate-notes (or an equivalent that produces notes).
|
|
46
51
|
- Uses git push --force-with-lease (not bare --force) for the post-rebase push.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# gitmoji — the full set, and the tooling that has to accept it
|
|
2
|
+
|
|
3
|
+
The commit rule lives in `../SKILL.md`: **every commit opens with a gitmoji, then the
|
|
4
|
+
Conventional Commits header.** This file is the lookup table and the tooling escape hatches — read
|
|
5
|
+
it when you need the right glyph for an intention, or when a parser in the repo rejects the format.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
<gitmoji> type(scope)!: imperative subject
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The gitmoji says *what kind of intention* this is at a glance in `git log --oneline`; the `type`
|
|
12
|
+
still drives the SemVer bump. They are two different jobs and neither replaces the other — a commit
|
|
13
|
+
with 💥 but no `!`/`BREAKING CHANGE:` footer still releases as a MINOR, because tooling reads the
|
|
14
|
+
type, not the picture.
|
|
15
|
+
|
|
16
|
+
## Picking one
|
|
17
|
+
|
|
18
|
+
Match the **intention**, not the file that changed. If two fit, take the more specific one (🚑️ over
|
|
19
|
+
🐛 for a production hotfix; 🩹 over 🐛 for a trivial non-critical fix). The `semver` column is
|
|
20
|
+
gitmoji's own hint, not a substitute for the type → bump table in `../SKILL.md`.
|
|
21
|
+
|
|
22
|
+
The everyday dozen, mapped to the conventional types:
|
|
23
|
+
|
|
24
|
+
| Type | gitmoji | Type | gitmoji |
|
|
25
|
+
|------------|---------|-------------|---------|
|
|
26
|
+
| `feat` | ✨ | `perf` | ⚡️ |
|
|
27
|
+
| `fix` | 🐛 | `ci` | 👷 |
|
|
28
|
+
| `docs` | 📝 | `build` | 📦️ |
|
|
29
|
+
| `refactor` | ♻️ | `style` | 🎨 |
|
|
30
|
+
| `test` | ✅ | `revert` | ⏪️ |
|
|
31
|
+
| `chore` | 🔧 | breaking | 💥 |
|
|
32
|
+
|
|
33
|
+
## The full set
|
|
34
|
+
|
|
35
|
+
Canonical source: <https://gitmoji.dev> (machine-readable at `https://gitmoji.dev/api/gitmojis`).
|
|
36
|
+
The same 75 rows are enforced as data by the gitmoji guard, and a test compares this table against
|
|
37
|
+
it — if the two ever disagree, CI says so.
|
|
38
|
+
|
|
39
|
+
| Emoji | Code | SemVer | Intention |
|
|
40
|
+
|-------|------|--------|-----------|
|
|
41
|
+
| 🎨 | `:art:` | — | Improve structure / format of the code |
|
|
42
|
+
| ⚡️ | `:zap:` | patch | Improve performance |
|
|
43
|
+
| 🔥 | `:fire:` | — | Remove code or files |
|
|
44
|
+
| 🐛 | `:bug:` | patch | Fix a bug |
|
|
45
|
+
| 🚑️ | `:ambulance:` | patch | Critical hotfix |
|
|
46
|
+
| ✨ | `:sparkles:` | minor | Introduce new features |
|
|
47
|
+
| 📝 | `:memo:` | — | Add or update documentation |
|
|
48
|
+
| 🚀 | `:rocket:` | — | Deploy stuff |
|
|
49
|
+
| 💄 | `:lipstick:` | patch | Add or update the UI and style files |
|
|
50
|
+
| 🎉 | `:tada:` | — | Begin a project |
|
|
51
|
+
| ✅ | `:white_check_mark:` | — | Add, update, or pass tests |
|
|
52
|
+
| 🔒️ | `:lock:` | patch | Fix security or privacy issues |
|
|
53
|
+
| 🔐 | `:closed_lock_with_key:` | — | Add or update secrets |
|
|
54
|
+
| 🔖 | `:bookmark:` | — | Release / Version tags |
|
|
55
|
+
| 🚨 | `:rotating_light:` | — | Fix compiler / linter warnings |
|
|
56
|
+
| 🚧 | `:construction:` | — | Work in progress |
|
|
57
|
+
| 💚 | `:green_heart:` | — | Fix CI Build |
|
|
58
|
+
| ⬇️ | `:arrow_down:` | patch | Downgrade dependencies |
|
|
59
|
+
| ⬆️ | `:arrow_up:` | patch | Upgrade dependencies |
|
|
60
|
+
| 📌 | `:pushpin:` | patch | Pin dependencies to specific versions |
|
|
61
|
+
| 👷 | `:construction_worker:` | — | Add or update CI build system |
|
|
62
|
+
| 📈 | `:chart_with_upwards_trend:` | patch | Add or update analytics or track code |
|
|
63
|
+
| ♻️ | `:recycle:` | — | Refactor code |
|
|
64
|
+
| ➕ | `:heavy_plus_sign:` | patch | Add a dependency |
|
|
65
|
+
| ➖ | `:heavy_minus_sign:` | patch | Remove a dependency |
|
|
66
|
+
| 🔧 | `:wrench:` | patch | Add or update configuration files |
|
|
67
|
+
| 🔨 | `:hammer:` | — | Add or update development scripts |
|
|
68
|
+
| 🌐 | `:globe_with_meridians:` | patch | Internationalization and localization |
|
|
69
|
+
| ✏️ | `:pencil2:` | patch | Fix typos |
|
|
70
|
+
| 💩 | `:poop:` | — | Write bad code that needs to be improved |
|
|
71
|
+
| ⏪️ | `:rewind:` | patch | Revert changes |
|
|
72
|
+
| 🔀 | `:twisted_rightwards_arrows:` | — | Merge branches |
|
|
73
|
+
| 📦️ | `:package:` | patch | Add or update compiled files or packages |
|
|
74
|
+
| 👽️ | `:alien:` | patch | Update code due to external API changes |
|
|
75
|
+
| 🚚 | `:truck:` | — | Move or rename resources (e.g.: files, paths, routes) |
|
|
76
|
+
| 📄 | `:page_facing_up:` | — | Add or update license |
|
|
77
|
+
| 💥 | `:boom:` | major | Introduce breaking changes |
|
|
78
|
+
| 🍱 | `:bento:` | patch | Add or update assets |
|
|
79
|
+
| ♿️ | `:wheelchair:` | patch | Improve accessibility |
|
|
80
|
+
| 💡 | `:bulb:` | — | Add or update comments in source code |
|
|
81
|
+
| 🍻 | `:beers:` | — | Write code drunkenly |
|
|
82
|
+
| 💬 | `:speech_balloon:` | patch | Add or update text and literals |
|
|
83
|
+
| 🗃️ | `:card_file_box:` | patch | Perform database related changes |
|
|
84
|
+
| 🔊 | `:loud_sound:` | — | Add or update logs |
|
|
85
|
+
| 🔇 | `:mute:` | — | Remove logs |
|
|
86
|
+
| 👥 | `:busts_in_silhouette:` | — | Add or update contributor(s) |
|
|
87
|
+
| 🚸 | `:children_crossing:` | patch | Improve user experience / usability |
|
|
88
|
+
| 🏗️ | `:building_construction:` | — | Make architectural changes |
|
|
89
|
+
| 📱 | `:iphone:` | patch | Work on responsive design |
|
|
90
|
+
| 🤡 | `:clown_face:` | — | Mock things |
|
|
91
|
+
| 🥚 | `:egg:` | patch | Add or update an easter egg |
|
|
92
|
+
| 🙈 | `:see_no_evil:` | — | Add or update a .gitignore file |
|
|
93
|
+
| 📸 | `:camera_flash:` | — | Add or update snapshots |
|
|
94
|
+
| ⚗️ | `:alembic:` | patch | Perform experiments |
|
|
95
|
+
| 🔍️ | `:mag:` | patch | Improve SEO |
|
|
96
|
+
| 🏷️ | `:label:` | patch | Add or update types |
|
|
97
|
+
| 🌱 | `:seedling:` | — | Add or update seed files |
|
|
98
|
+
| 🚩 | `:triangular_flag_on_post:` | patch | Add, update, or remove feature flags |
|
|
99
|
+
| 🥅 | `:goal_net:` | patch | Catch errors |
|
|
100
|
+
| 💫 | `:dizzy:` | patch | Add or update animations and transitions |
|
|
101
|
+
| 🗑️ | `:wastebasket:` | patch | Deprecate code that needs to be cleaned up |
|
|
102
|
+
| 🛂 | `:passport_control:` | patch | Work on code related to authorization, roles and permissions |
|
|
103
|
+
| 🩹 | `:adhesive_bandage:` | patch | Simple fix for a non-critical issue |
|
|
104
|
+
| 🧐 | `:monocle_face:` | — | Data exploration/inspection |
|
|
105
|
+
| ⚰️ | `:coffin:` | — | Remove dead code |
|
|
106
|
+
| 🧪 | `:test_tube:` | — | Add a failing test |
|
|
107
|
+
| 👔 | `:necktie:` | patch | Add or update business logic |
|
|
108
|
+
| 🩺 | `:stethoscope:` | — | Add or update healthcheck |
|
|
109
|
+
| 🧱 | `:bricks:` | — | Infrastructure related changes |
|
|
110
|
+
| 🧑💻 | `:technologist:` | — | Improve developer experience |
|
|
111
|
+
| 💸 | `:money_with_wings:` | — | Add sponsorships or money related infrastructure |
|
|
112
|
+
| 🧵 | `:thread:` | — | Add or update code related to multithreading or concurrency |
|
|
113
|
+
| 🦺 | `:safety_vest:` | — | Add or update code related to validation |
|
|
114
|
+
| ✈️ | `:airplane:` | — | Improve offline support |
|
|
115
|
+
| 🦖 | `:t-rex:` | — | Code that adds backwards compatibility |
|
|
116
|
+
## Tooling that has to accept the format
|
|
117
|
+
|
|
118
|
+
Emoji-first is what gitmoji itself prescribes and what `gitmoji -c` writes, but a **strict
|
|
119
|
+
Conventional Commits parser anchors the type at position 0** and will reject the header. Two honest
|
|
120
|
+
ways out, in order of preference:
|
|
121
|
+
|
|
122
|
+
**1. Teach the parser about the prefix.** commitlint, with the emoji allowed before the type:
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
// commitlint.config.js
|
|
126
|
+
export default {
|
|
127
|
+
extends: ['@commitlint/config-conventional'],
|
|
128
|
+
parserPreset: {
|
|
129
|
+
parserOpts: {
|
|
130
|
+
// <emoji> type(scope)!: subject
|
|
131
|
+
headerPattern: /^(?:\S+\s)?(\w+)(?:\(([^)]*)\))?(!)?: (.+)$/,
|
|
132
|
+
headerCorrespondence: ['type', 'scope', 'breaking', 'subject'],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
semantic-release / conventional-changelog take the same `parserOpts` under
|
|
139
|
+
`@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator`. Set it in
|
|
140
|
+
**both** or the bump and the changelog disagree about the same commit.
|
|
141
|
+
|
|
142
|
+
**2. Put the emoji after the header** — `feat(api): ✨ add cursor paging`. Every conventional parser
|
|
143
|
+
accepts it unmodified, and the guard accepts it too. Prefer this in a repo whose release pipeline
|
|
144
|
+
you do not own.
|
|
145
|
+
|
|
146
|
+
What **not** to do: drop the type and commit `✨ add cursor paging` (gitmoji's own minimal form).
|
|
147
|
+
It is valid gitmoji and useless for releases — nothing can derive the bump, so the version has to be
|
|
148
|
+
hand-picked, which `../SKILL.md` forbids for a reason.
|
|
149
|
+
|
|
150
|
+
## Related
|
|
151
|
+
|
|
152
|
+
- `gitmoji-cli` (`npm i -g gitmoji-cli`, then `gitmoji -c`) prompts for the emoji and writes the
|
|
153
|
+
commit. Optional — the convention is the rule, the CLI is one way to type it.
|
|
154
|
+
- On Claude Code, rsc wires a `gitmoji-guard` PreToolUse hook that denies a `git commit` whose
|
|
155
|
+
message has no gitmoji, with the corrected message in the refusal. It only judges messages it can
|
|
156
|
+
actually read (`-m`, heredoc) and never blocks an editor commit, a `-F` file, or `--amend
|
|
157
|
+
--no-edit`. Opt out per project with `.rsc/.no-gitmoji`.
|
package/skills/ship/SKILL.md
CHANGED
|
@@ -184,7 +184,7 @@ cleanup). If a native `EnterWorktree`-style tool created it, exit through that t
|
|
|
184
184
|
|
|
185
185
|
The commit is the durable record. Make it describe the change and tie it to the spec — and keep it Eric's.
|
|
186
186
|
|
|
187
|
-
- **Subject:**
|
|
187
|
+
- **Subject:** `<gitmoji> type: imperative summary (<spec-slug>)` — `✨ feat:`, `🐛 fix:`, `♻️ refactor:`, etc. Under ~72 chars. The gitmoji is **not optional**: on Claude Code a PreToolUse guard denies a `git commit -m` without one, and the refusal hands back the corrected message. Emoji → intention table: `../git-workflow/references/gitmoji.md`.
|
|
188
188
|
- **Body:** *why*, not a restatement of the diff. Reference the spec slug and any decision logged in `decisions.md`.
|
|
189
189
|
- **Footer:** issue/PR refs only. **No `Co-Authored-By` for any AI. No "generated with" line.** This is where the violation usually sneaks in — leave the footer clean.
|
|
190
190
|
|
|
@@ -212,6 +212,7 @@ Read the level from `02-DOCS/wiki/harness/user-profile.md`. It changes what you
|
|
|
212
212
|
| "The tree has a couple of stray edits, they're harmless" | A dirty tree means the merge is not the reviewed diff. Clean it or stash it first. |
|
|
213
213
|
| "`main` is protected but I'll just force-merge, I'm sure" | Protected means PR. Don't bypass the gate the repo deliberately set. |
|
|
214
214
|
| "I'll rebase and resolve conflicts during the merge" | Resolve before. A conflicted merge commit hides what actually shipped. |
|
|
215
|
+
| "The gitmoji is decoration, the conventional type is what matters" | Both ship or neither does. The type is for tooling, the emoji for the human scanning `git log` — and the guard denies the commit either way. |
|
|
215
216
|
| "This branch is dead, I'll just delete it" | Discard is destructive — confirm with the quoted branch name and log why first. |
|
|
216
217
|
| "Squash everything, history doesn't matter" | Squash noise, preserve meaning. The history is the next reader's spec. |
|
|
217
218
|
| "There's a key in the diff but it's a test key" | A secret in the diff is a blocker regardless. Pull it out before landing. |
|
package/targets/claude.js
CHANGED
|
@@ -113,6 +113,21 @@ export function wireHook(paths) {
|
|
|
113
113
|
);
|
|
114
114
|
settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [{ type: 'command', command: dgCmd }] });
|
|
115
115
|
|
|
116
|
+
// Gitmoji guard: a PreToolUse(Bash) hook that DENIES a `git commit` whose message
|
|
117
|
+
// carries no gitmoji (gitmoji.dev) in front of the Conventional Commits header. The
|
|
118
|
+
// convention lives in `git-workflow`; prose asks, this hook decides — at the one
|
|
119
|
+
// deterministic moment the message exists. Only fires when the message is inline and
|
|
120
|
+
// readable (-m / heredoc); editor commits, -F files and --amend --no-edit are allowed.
|
|
121
|
+
// Materialized + node-run (Windows-safe), idempotent, fail-open, opt-out via
|
|
122
|
+
// .rsc/.no-gitmoji.
|
|
123
|
+
const gmDest = join(paths.projectRoot, '.rsc', 'gitmoji-guard.mjs');
|
|
124
|
+
copyFileSync(join(HERE, 'gitmoji-guard.mjs'), gmDest);
|
|
125
|
+
const gmCmd = `node "${gmDest}" "${paths.projectRoot}"`;
|
|
126
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
127
|
+
(e) => !JSON.stringify(e).includes('.rsc/gitmoji-guard.'),
|
|
128
|
+
);
|
|
129
|
+
settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [{ type: 'command', command: gmCmd }] });
|
|
130
|
+
|
|
116
131
|
// New-feature gate: a UserPromptSubmit hook that re-injects the SDD new-feature gate as
|
|
117
132
|
// the most-recent instruction on every user turn. SessionStart injects the full gate (the
|
|
118
133
|
// `suggest` body) once; this keeps the precedence rule salient regardless of how many
|
|
@@ -132,5 +147,5 @@ export function wireHook(paths) {
|
|
|
132
147
|
|
|
133
148
|
mkdirSync(dirname(file), { recursive: true });
|
|
134
149
|
writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
|
|
135
|
-
return [file, scriptDest, wlDest, sgDest, dgDest, fgDest];
|
|
150
|
+
return [file, scriptDest, wlDest, sgDest, dgDest, gmDest, fgDest];
|
|
136
151
|
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// rsc Gitmoji guard (claude). Wired by targets/claude.js onto PreToolUse (matcher Bash)
|
|
3
|
+
// as `node ...` so it runs on every platform including Windows.
|
|
4
|
+
// argv[2] = absolute project root stdin = PreToolUse hook JSON
|
|
5
|
+
//
|
|
6
|
+
// Enforces one convention at the only deterministic moment it can be enforced: the
|
|
7
|
+
// commit is being written. Every commit message this harness produces carries a
|
|
8
|
+
// gitmoji (https://gitmoji.dev) — the intention of the change, visible in one glyph
|
|
9
|
+
// in `git log --oneline`, on top of the Conventional Commits grammar that already
|
|
10
|
+
// drives the semver bump. Prose in a skill asks; this hook decides.
|
|
11
|
+
//
|
|
12
|
+
// Accepted shapes (both are gitmoji-valid; the first is what `git-workflow` prescribes):
|
|
13
|
+
// ✨ feat(api): add cursor paging
|
|
14
|
+
// feat(api): ✨ add cursor paging
|
|
15
|
+
//
|
|
16
|
+
// Design (constitution P1/P2/P6/P7): deterministic, local-only, precise — it fires only
|
|
17
|
+
// on a `git commit` that carries its message inline, where the message can actually be
|
|
18
|
+
// read. Anything it cannot read with certainty (editor commit, -F file, --amend
|
|
19
|
+
// --no-edit, an unparseable command) is ALLOWED: a guard that guesses is a guard that
|
|
20
|
+
// gets turned off. Every deny names its recovery. Opt out with .rsc/.no-gitmoji.
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
|
|
24
|
+
// The official gitmoji set (gitmoji.dev/api/gitmojis), as data: [emoji, code, semver, meaning].
|
|
25
|
+
// A table, not a chain of ifs — so a test can iterate it and the reference doc can be
|
|
26
|
+
// checked against it instead of drifting apart in prose.
|
|
27
|
+
export const GITMOJIS = [
|
|
28
|
+
['🎨', ':art:', null, "Improve structure / format of the code."],
|
|
29
|
+
['⚡️', ':zap:', 'patch', "Improve performance."],
|
|
30
|
+
['🔥', ':fire:', null, "Remove code or files."],
|
|
31
|
+
['🐛', ':bug:', 'patch', "Fix a bug."],
|
|
32
|
+
['🚑️', ':ambulance:', 'patch', "Critical hotfix."],
|
|
33
|
+
['✨', ':sparkles:', 'minor', "Introduce new features."],
|
|
34
|
+
['📝', ':memo:', null, "Add or update documentation."],
|
|
35
|
+
['🚀', ':rocket:', null, "Deploy stuff."],
|
|
36
|
+
['💄', ':lipstick:', 'patch', "Add or update the UI and style files."],
|
|
37
|
+
['🎉', ':tada:', null, "Begin a project."],
|
|
38
|
+
['✅', ':white_check_mark:', null, "Add, update, or pass tests."],
|
|
39
|
+
['🔒️', ':lock:', 'patch', "Fix security or privacy issues."],
|
|
40
|
+
['🔐', ':closed_lock_with_key:', null, "Add or update secrets."],
|
|
41
|
+
['🔖', ':bookmark:', null, "Release / Version tags."],
|
|
42
|
+
['🚨', ':rotating_light:', null, "Fix compiler / linter warnings."],
|
|
43
|
+
['🚧', ':construction:', null, "Work in progress."],
|
|
44
|
+
['💚', ':green_heart:', null, "Fix CI Build."],
|
|
45
|
+
['⬇️', ':arrow_down:', 'patch', "Downgrade dependencies."],
|
|
46
|
+
['⬆️', ':arrow_up:', 'patch', "Upgrade dependencies."],
|
|
47
|
+
['📌', ':pushpin:', 'patch', "Pin dependencies to specific versions."],
|
|
48
|
+
['👷', ':construction_worker:', null, "Add or update CI build system."],
|
|
49
|
+
['📈', ':chart_with_upwards_trend:', 'patch', "Add or update analytics or track code."],
|
|
50
|
+
['♻️', ':recycle:', null, "Refactor code."],
|
|
51
|
+
['➕', ':heavy_plus_sign:', 'patch', "Add a dependency."],
|
|
52
|
+
['➖', ':heavy_minus_sign:', 'patch', "Remove a dependency."],
|
|
53
|
+
['🔧', ':wrench:', 'patch', "Add or update configuration files."],
|
|
54
|
+
['🔨', ':hammer:', null, "Add or update development scripts."],
|
|
55
|
+
['🌐', ':globe_with_meridians:', 'patch', "Internationalization and localization."],
|
|
56
|
+
['✏️', ':pencil2:', 'patch', "Fix typos."],
|
|
57
|
+
['💩', ':poop:', null, "Write bad code that needs to be improved."],
|
|
58
|
+
['⏪️', ':rewind:', 'patch', "Revert changes."],
|
|
59
|
+
['🔀', ':twisted_rightwards_arrows:', null, "Merge branches."],
|
|
60
|
+
['📦️', ':package:', 'patch', "Add or update compiled files or packages."],
|
|
61
|
+
['👽️', ':alien:', 'patch', "Update code due to external API changes."],
|
|
62
|
+
['🚚', ':truck:', null, "Move or rename resources (e.g.: files, paths, routes)."],
|
|
63
|
+
['📄', ':page_facing_up:', null, "Add or update license."],
|
|
64
|
+
['💥', ':boom:', 'major', "Introduce breaking changes."],
|
|
65
|
+
['🍱', ':bento:', 'patch', "Add or update assets."],
|
|
66
|
+
['♿️', ':wheelchair:', 'patch', "Improve accessibility."],
|
|
67
|
+
['💡', ':bulb:', null, "Add or update comments in source code."],
|
|
68
|
+
['🍻', ':beers:', null, "Write code drunkenly."],
|
|
69
|
+
['💬', ':speech_balloon:', 'patch', "Add or update text and literals."],
|
|
70
|
+
['🗃️', ':card_file_box:', 'patch', "Perform database related changes."],
|
|
71
|
+
['🔊', ':loud_sound:', null, "Add or update logs."],
|
|
72
|
+
['🔇', ':mute:', null, "Remove logs."],
|
|
73
|
+
['👥', ':busts_in_silhouette:', null, "Add or update contributor(s)."],
|
|
74
|
+
['🚸', ':children_crossing:', 'patch', "Improve user experience / usability."],
|
|
75
|
+
['🏗️', ':building_construction:', null, "Make architectural changes."],
|
|
76
|
+
['📱', ':iphone:', 'patch', "Work on responsive design."],
|
|
77
|
+
['🤡', ':clown_face:', null, "Mock things."],
|
|
78
|
+
['🥚', ':egg:', 'patch', "Add or update an easter egg."],
|
|
79
|
+
['🙈', ':see_no_evil:', null, "Add or update a .gitignore file."],
|
|
80
|
+
['📸', ':camera_flash:', null, "Add or update snapshots."],
|
|
81
|
+
['⚗️', ':alembic:', 'patch', "Perform experiments."],
|
|
82
|
+
['🔍️', ':mag:', 'patch', "Improve SEO."],
|
|
83
|
+
['🏷️', ':label:', 'patch', "Add or update types."],
|
|
84
|
+
['🌱', ':seedling:', null, "Add or update seed files."],
|
|
85
|
+
['🚩', ':triangular_flag_on_post:', 'patch', "Add, update, or remove feature flags."],
|
|
86
|
+
['🥅', ':goal_net:', 'patch', "Catch errors."],
|
|
87
|
+
['💫', ':dizzy:', 'patch', "Add or update animations and transitions."],
|
|
88
|
+
['🗑️', ':wastebasket:', 'patch', "Deprecate code that needs to be cleaned up."],
|
|
89
|
+
['🛂', ':passport_control:', 'patch', "Work on code related to authorization, roles and permissions."],
|
|
90
|
+
['🩹', ':adhesive_bandage:', 'patch', "Simple fix for a non-critical issue."],
|
|
91
|
+
['🧐', ':monocle_face:', null, "Data exploration/inspection."],
|
|
92
|
+
['⚰️', ':coffin:', null, "Remove dead code."],
|
|
93
|
+
['🧪', ':test_tube:', null, "Add a failing test."],
|
|
94
|
+
['👔', ':necktie:', 'patch', "Add or update business logic."],
|
|
95
|
+
['🩺', ':stethoscope:', null, "Add or update healthcheck."],
|
|
96
|
+
['🧱', ':bricks:', null, "Infrastructure related changes."],
|
|
97
|
+
['🧑💻', ':technologist:', null, "Improve developer experience."],
|
|
98
|
+
['💸', ':money_with_wings:', null, "Add sponsorships or money related infrastructure."],
|
|
99
|
+
['🧵', ':thread:', null, "Add or update code related to multithreading or concurrency."],
|
|
100
|
+
['🦺', ':safety_vest:', null, "Add or update code related to validation."],
|
|
101
|
+
['✈️', ':airplane:', null, "Improve offline support."],
|
|
102
|
+
['🦖', ':t-rex:', null, "Code that adds backwards compatibility."],];
|
|
103
|
+
|
|
104
|
+
// Some official gitmojis carry U+FE0F (⚡️, 🔒️, 🗑️ …). A human typing the bare
|
|
105
|
+
// code point means the same emoji, so compare with the variation selector stripped —
|
|
106
|
+
// otherwise the guard would reject a correct commit over an invisible byte.
|
|
107
|
+
const bare = (s) => s.replace(/️/g, '');
|
|
108
|
+
export const EMOJI_SET = new Set(GITMOJIS.map(([e]) => bare(e)));
|
|
109
|
+
export const CODE_SET = new Set(GITMOJIS.map(([, c]) => c));
|
|
110
|
+
|
|
111
|
+
// git's own functional prefixes: these messages are consumed by `rebase --autosquash`,
|
|
112
|
+
// not read by humans, and the final message is the target commit's. Nothing to enforce.
|
|
113
|
+
const GIT_MAGIC = /^(fixup|squash|amend)!/;
|
|
114
|
+
// A Conventional Commits header, optionally present before the emoji.
|
|
115
|
+
const CONVENTIONAL = /^[a-zA-Z]+(\([^)\n]*\))?!?:\s*/;
|
|
116
|
+
|
|
117
|
+
// Does this subject line open with a gitmoji — either before the conventional prefix
|
|
118
|
+
// or immediately after it? An emoji buried mid-sentence is not an intention marker.
|
|
119
|
+
export function hasGitmoji(message) {
|
|
120
|
+
const subject = String(message ?? '').split(/\r?\n/, 1)[0].trim();
|
|
121
|
+
if (!subject) return false;
|
|
122
|
+
if (GIT_MAGIC.test(subject)) return true;
|
|
123
|
+
const candidates = [subject, subject.replace(CONVENTIONAL, '')];
|
|
124
|
+
for (const c of candidates) {
|
|
125
|
+
const head = bare(c);
|
|
126
|
+
for (const e of EMOJI_SET) if (head.startsWith(e)) return true;
|
|
127
|
+
const code = head.match(/^:[a-z0-9_+-]+:/);
|
|
128
|
+
if (code && CODE_SET.has(code[0])) return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- reading the message out of a shell command --------------------------------
|
|
134
|
+
// Shell is not parseable in a regex and we do not pretend otherwise: this recognizes
|
|
135
|
+
// the forms an agent or a human actually types, and returns null ("cannot tell") for
|
|
136
|
+
// everything else so the caller allows it.
|
|
137
|
+
|
|
138
|
+
const COMMIT = /\bgit\b(?:\s+-{1,2}\S+(?:\s+\S+)?)*\s+commit\b/;
|
|
139
|
+
const NO_MESSAGE_TO_READ = /(?:^|\s)(?:-F|--file|--template|-C\s+\S*HEAD|--reuse-message|--reedit-message)\b/;
|
|
140
|
+
const AMEND_NO_EDIT = /--no-edit\b/;
|
|
141
|
+
const HAS_INLINE_M = /(?:^|\s)(?:-[a-zA-Z]*m|--message)(?:=|\s|$)/;
|
|
142
|
+
// `git` must be the command being RUN, not text inside someone else's argument —
|
|
143
|
+
// otherwise `echo "git commit -m x"` or `grep "git commit -m"` gets denied, and a guard
|
|
144
|
+
// that blocks the discussion of commits is exactly the kind that gets switched off.
|
|
145
|
+
const RUNS_GIT = /^\s*(?:sudo\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*git\b/;
|
|
146
|
+
|
|
147
|
+
const SEPARATORS = /&&|\|\||[;\n]/;
|
|
148
|
+
|
|
149
|
+
// Is this segment a `git commit` that carries its message inline and readably?
|
|
150
|
+
function readableCommit(segment) {
|
|
151
|
+
return RUNS_GIT.test(segment)
|
|
152
|
+
&& COMMIT.test(segment)
|
|
153
|
+
&& HAS_INLINE_M.test(segment)
|
|
154
|
+
&& !NO_MESSAGE_TO_READ.test(segment)
|
|
155
|
+
&& !AMEND_NO_EDIT.test(segment);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// `-m "$(cat <<'EOF' … EOF)"` — the multi-line form. The heredoc body IS the message,
|
|
159
|
+
// and it may contain && or ; so it must be lifted out before any splitting.
|
|
160
|
+
function heredocBody(command) {
|
|
161
|
+
const m = command.match(/<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?\r?\n([\s\S]*?)\r?\n[ \t]*\1\b/);
|
|
162
|
+
return m ? m[2] : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// -m/--message value, quoted or bare.
|
|
166
|
+
function inlineMessage(segment) {
|
|
167
|
+
const m = segment.match(/(?:^|\s)(?:-[a-zA-Z]*m|--message)(?:=|\s+)("([^"]*)"|'([^']*)'|([^\s'"][^\s]*))/);
|
|
168
|
+
if (!m) return null;
|
|
169
|
+
return m[2] ?? m[3] ?? m[4] ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Every commit message this command would write, or null when nothing is checkable.
|
|
173
|
+
export function commitMessages(command) {
|
|
174
|
+
const cmd = String(command ?? '');
|
|
175
|
+
if (!COMMIT.test(cmd)) return [];
|
|
176
|
+
|
|
177
|
+
// The heredoc form must be handled before any splitting: its body is the message and
|
|
178
|
+
// may legitimately contain && or ;. The command that owns it is whatever runs just
|
|
179
|
+
// before the `<<`.
|
|
180
|
+
const hIdx = cmd.search(/<<-?\s*['"]?[A-Za-z_]/);
|
|
181
|
+
if (hIdx !== -1) {
|
|
182
|
+
const owner = cmd.slice(0, hIdx).split(SEPARATORS).pop() || '';
|
|
183
|
+
if (!readableCommit(owner)) return [];
|
|
184
|
+
const body = heredocBody(cmd);
|
|
185
|
+
if (body === null) return [];
|
|
186
|
+
const first = body.split(/\r?\n/).find((l) => l.trim() !== '');
|
|
187
|
+
return first === undefined ? [] : [first];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const out = [];
|
|
191
|
+
for (const segment of cmd.split(SEPARATORS)) {
|
|
192
|
+
if (!readableCommit(segment)) continue;
|
|
193
|
+
const msg = inlineMessage(segment);
|
|
194
|
+
if (msg === null) continue; // editor commit — cannot read it here
|
|
195
|
+
if (/^\$/.test(msg.trim())) continue; // a variable/substitution, not a literal
|
|
196
|
+
out.push(msg);
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- the deny message (P6: the recovery lives inside the refusal) ---------------
|
|
202
|
+
|
|
203
|
+
export const SUGGESTIONS = [
|
|
204
|
+
['feat', '✨'], ['fix', '🐛'], ['docs', '📝'], ['refactor', '♻️'], ['test', '✅'],
|
|
205
|
+
['perf', '⚡️'], ['chore', '🔧'], ['ci', '👷'], ['style', '🎨'], ['revert', '⏪️'],
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
export function denyMessage(message) {
|
|
209
|
+
const subject = String(message ?? '').split(/\r?\n/, 1)[0].trim();
|
|
210
|
+
const type = subject.match(/^([a-zA-Z]+)(\([^)\n]*\))?!?:/)?.[1]?.toLowerCase();
|
|
211
|
+
const hit = SUGGESTIONS.find(([t]) => t === type);
|
|
212
|
+
const rewrite = hit
|
|
213
|
+
? `${hit[1]} ${subject}`
|
|
214
|
+
: `${SUGGESTIONS.find(([t]) => t === 'chore')[1]} ${subject || '<type>(<scope>): <subject>'}`;
|
|
215
|
+
return [
|
|
216
|
+
`This commit message has no gitmoji: "${subject}".`,
|
|
217
|
+
'Every commit in this project opens with a gitmoji (gitmoji.dev) before the Conventional Commits header — the intention of the change, readable in one glyph in `git log --oneline`.',
|
|
218
|
+
`Recover: re-run the commit with the emoji in front — \`${rewrite}\` — picking the gitmoji that matches the intention (✨ feature · 🐛 fix · 📝 docs · ♻️ refactor · ✅ tests · ⚡️ perf · 🔧 config · 👷 CI · 💥 breaking · 🔖 release). The full table is in the \`git-workflow\` skill (references/gitmoji.md). To turn this guard off for this project, create .rsc/.no-gitmoji.`,
|
|
219
|
+
].join('\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---- hook entrypoint -----------------------------------------------------------
|
|
223
|
+
// Skipped when imported by a test (P2: the mechanism is testable without a subprocess).
|
|
224
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
225
|
+
const root = process.argv[2] || process.cwd();
|
|
226
|
+
const allow = () => process.exit(0);
|
|
227
|
+
|
|
228
|
+
if (existsSync(join(root, '.rsc', '.no-gitmoji'))) allow();
|
|
229
|
+
|
|
230
|
+
let input = {};
|
|
231
|
+
try { input = JSON.parse(readFileSync(0, 'utf8') || '{}'); } catch { allow(); }
|
|
232
|
+
if ((input.tool_name || input.toolName) !== 'Bash') allow();
|
|
233
|
+
const command = input.tool_input?.command || input.toolInput?.command || '';
|
|
234
|
+
if (typeof command !== 'string' || !command) allow();
|
|
235
|
+
|
|
236
|
+
let offender = null;
|
|
237
|
+
try {
|
|
238
|
+
offender = commitMessages(command).find((m) => !hasGitmoji(m)) ?? null;
|
|
239
|
+
} catch { allow(); } // any parsing surprise → allow (fail open, by design)
|
|
240
|
+
if (offender === null) allow();
|
|
241
|
+
|
|
242
|
+
process.stdout.write(JSON.stringify({
|
|
243
|
+
hookSpecificOutput: {
|
|
244
|
+
hookEventName: 'PreToolUse',
|
|
245
|
+
permissionDecision: 'deny',
|
|
246
|
+
permissionDecisionReason: denyMessage(offender),
|
|
247
|
+
},
|
|
248
|
+
}));
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|