@ericrisco/rsc 1.0.25 → 1.0.27

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 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.25",
2
+ "version": "1.0.27",
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
  ],
@@ -4556,7 +4557,7 @@
4556
4557
  },
4557
4558
  {
4558
4559
  "id": "specify",
4559
- "description": "Use when a feature, change, or product idea is still fuzzy and must become an approved WHAT/WHY spec before any planning or code — the brainstorming front door of SDD. Turns a one-line intent into problem, goals, users, scope, behaviour and acceptance criteria, via one-question-at-a-time dialogue and 2-3 approaches with a recommendation, carrying zero implementation detail. Fires whenever someone jumps to HOW before WHAT is agreed, in any language. NOT the technical plan (that is `plan`), NOT the de-risking ambiguity sweep (that is `clarify`), NOT project-wide principles (that is `constitution`).",
4560
+ "description": "Use when a feature, change, or product idea is still fuzzy and must become an approved WHAT/WHY spec before any planning or code — the brainstorming front door of SDD. Turns a one-line intent into problem, goals, users, scope, behaviour and acceptance criteria, via frontier-round dialogue and 2-3 approaches with a recommendation, carrying zero implementation detail. Fires whenever someone jumps to HOW before WHAT is agreed, in any language. NOT the technical plan (that is `plan`), NOT the de-risking ambiguity sweep (that is `clarify`), NOT project-wide principles (that is `constitution`).",
4560
4561
  "tags": [
4561
4562
  "sdd",
4562
4563
  "spec",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  "drift:check": "node scripts/drift-check.js",
18
18
  "drift:knowledge": "node scripts/drift-check.js --knowledge",
19
19
  "knowledge:doctor": "node scripts/knowledge-doctor.js",
20
+ "spec:gate": "node scripts/spec-gate.js",
20
21
  "routing:audit": "node scripts/routing-audit.js",
21
22
  "test": "node --test",
22
23
  "coverage": "c8 --reporter=text node --test",
@@ -3,14 +3,15 @@ import { join, dirname } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import Ajv from 'ajv';
5
5
  import { parseFrontmatter } from './lib/frontmatter.js';
6
+ import { fenceBalance } from './lib/skill-lint.js';
6
7
 
7
8
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
8
9
  const SKILLS = join(ROOT, 'skills');
9
10
 
10
- function skillDirs() {
11
- return readdirSync(SKILLS).filter((d) => {
11
+ function skillDirs(base = SKILLS) {
12
+ return readdirSync(base).filter((d) => {
12
13
  try {
13
- return statSync(join(SKILLS, d)).isDirectory() && statSync(join(SKILLS, d, 'SKILL.md')).isFile();
14
+ return statSync(join(base, d)).isDirectory() && statSync(join(base, d, 'SKILL.md')).isFile();
14
15
  } catch {
15
16
  return false;
16
17
  }
@@ -47,6 +48,20 @@ export function validateFrontmatter() {
47
48
  return errors;
48
49
  }
49
50
 
51
+ // A skill body whose code-block delimiters do not pair renders half its content as one code block,
52
+ // and every other gate stays green while it happens. Deterministic defect, deterministic check —
53
+ // and it lives here so `prepublishOnly` can never ship one.
54
+ export function validateBodies(base = SKILLS) {
55
+ const errors = [];
56
+ for (const id of skillDirs(base)) {
57
+ const r = fenceBalance(readFileSync(join(base, id, 'SKILL.md'), 'utf8'));
58
+ if (!r.balanced) {
59
+ errors.push(`${id}: unbalanced code-block delimiter opened at line ${r.opened} (${r.fences} found)`);
60
+ }
61
+ }
62
+ return errors;
63
+ }
64
+
50
65
  // A skill's `description` is in context on every turn it is installed, invoked or not, so the
51
66
  // catalog's total description weight is the one number that scales with the catalog itself.
52
67
  // The hard failure stays at the schema limit; this reports the soft ceiling the rubric asks for,
@@ -67,9 +82,10 @@ function main() {
67
82
  const arg = process.argv[2];
68
83
  const out = join(ROOT, 'manifest.json');
69
84
  if (arg === '--validate') {
70
- const errs = validateFrontmatter();
85
+ const errs = [...validateFrontmatter(), ...validateBodies()];
71
86
  if (errs.length) { console.error(errs.join('\n')); process.exit(1); }
72
87
  console.log('frontmatter OK');
88
+ console.log('skill bodies OK (code-block delimiters balanced)');
73
89
  const w = descriptionWeight();
74
90
  console.log(
75
91
  `description weight: ${(w.total / 1024).toFixed(1)} KB across ${w.count} skills `
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
  };
@@ -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'),
@@ -0,0 +1,29 @@
1
+ // Structural lint of a skill body. Pure: give it the text, get a verdict.
2
+ //
3
+ // Scope is deliberately one defect class. A stray closing delimiter inverts every block boundary
4
+ // after it, so a skill can render half its body as one code block while every other gate stays
5
+ // green: the frontmatter validates, the description fits, the prose reads fine in the source. It is
6
+ // a purely deterministic defect, so it belongs to a binary, not to a reader's attention.
7
+
8
+ const FENCE = /^(?:```|~~~)/;
9
+
10
+ /**
11
+ * @param {string} body raw skill markdown
12
+ * @returns {{balanced: boolean, fences: number, opened: number|null}}
13
+ * `fences` counts column-0 delimiters; `opened` is the 1-indexed line of the unpaired one.
14
+ */
15
+ export function fenceBalance(body) {
16
+ const lines = String(body ?? '').split('\n');
17
+ let open = null; // line number of the delimiter that opened the current block
18
+ let fences = 0;
19
+
20
+ for (let i = 0; i < lines.length; i += 1) {
21
+ // Only a column-0 delimiter is a block boundary. Indented ones are content: a fenced sample
22
+ // inside a fenced block. Counting those would flag correct documents.
23
+ if (!FENCE.test(lines[i])) continue;
24
+ fences += 1;
25
+ open = open === null ? i + 1 : null;
26
+ }
27
+
28
+ return { balanced: open === null, fences, opened: open };
29
+ }
@@ -0,0 +1,122 @@
1
+ // The exit gate of the `specify` phase, as a binary instead of a feeling.
2
+ //
3
+ // "Stop when the WHAT and WHY are complete enough to plan against" cannot tell done from not-done,
4
+ // so the phase closed whenever the agent felt closed — with the write/review/approve steps visible
5
+ // the whole time, pulling attention toward being finished. This checks the artifact instead: every
6
+ // template section carries content or an explicit open point.
7
+ //
8
+ // What it does NOT check is returned in `unchecked`, on purpose. Detecting an unmarked assumption is
9
+ // the problem `specify` exists to solve, not something a parser can do, and a gate that implies
10
+ // otherwise is worse than one that admits its edge.
11
+
12
+ // The four states a point can be in. Order is the table's order in the skill body.
13
+ export const OPEN_POINT_TYPES = [
14
+ 'pregunta abierta', // formulable now, unanswered → clarify asks it
15
+ 'suposición tomada', // we decided, with the basis written → clarify validates it
16
+ 'decisión diferida', // sharp, out of this cycle on purpose → clarify leaves it
17
+ 'área no formulable', // known to be coming, not yet sharp → clarify notes it
18
+ ];
19
+
20
+ // The corpus is Spanish, the template is English, and both carry drifting parentheticals
21
+ // ("Objetivos (resultado)", "Criterios de aceptación (binarios)"). Matching literal strings would
22
+ // be useless, so each family is a set of normalised prefixes.
23
+ const FAMILIES = {
24
+ problem: ['problema & por que', 'problema y por que', 'problem & why'],
25
+ goals: ['objetivos', 'goals'],
26
+ nongoals: ['no-objetivos', 'no objetivos', 'non-goals'],
27
+ users: ['usuarios & contexto', 'usuarios y contexto', 'users & context'],
28
+ behaviour: ['comportamiento', 'behaviour', 'behavior'],
29
+ acceptance: ['criterios de aceptacion', 'acceptance criteria'],
30
+ open: ['puntos a clarificar', 'puntos de clarify', 'puntos que siguen abiertos', 'points to clarify'],
31
+ };
32
+
33
+ const UNCHECKED = [
34
+ 'que ninguna sección contenga una suposición sin marcar (no es detectable por máquina; es el juicio que la fase existe para ejercer)',
35
+ 'que el contenido de cada sección sea correcto, y no sólo presente',
36
+ 'que los criterios de aceptación sean de verdad binarios',
37
+ ];
38
+
39
+ function fold(s) {
40
+ return s
41
+ .normalize('NFD')
42
+ .replace(/[̀-ͯ]/g, '')
43
+ .toLowerCase()
44
+ .trim();
45
+ }
46
+
47
+ function familyOf(heading) {
48
+ const h = fold(heading);
49
+ for (const [family, prefixes] of Object.entries(FAMILIES)) {
50
+ if (prefixes.some((p) => h.startsWith(p))) return family;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ // A heading with nothing under it is not a filled section. Neither is one holding only the
56
+ // template's own italic/angle-bracket guidance, which is what a hurried pass leaves behind.
57
+ function isSubstantive(line) {
58
+ const t = line.trim();
59
+ if (!t) return false;
60
+ if (/^<.*>$/.test(t)) return false; // <guidance from the template>
61
+ if (/^\*[^*].*\*$/.test(t) && t.length < 200) return false; // *italic guidance*
62
+ if (/^(-|\*)\s*$/.test(t)) return false; // an empty bullet
63
+ return true;
64
+ }
65
+
66
+ function parseOpenPoints(lines) {
67
+ const points = [];
68
+ for (const line of lines) {
69
+ const m = line.match(/^\s*[-*]\s+(?:\[[ x]\]\s*)?(.*)$/);
70
+ if (!m || !m[1].trim()) continue;
71
+ const body = m[1];
72
+ const declared = OPEN_POINT_TYPES.find((t) => {
73
+ const re = new RegExp(`^\\*\\*\\s*${t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\*\\*`, 'i');
74
+ return re.test(fold(body)) || re.test(body);
75
+ });
76
+ points.push({
77
+ type: declared || OPEN_POINT_TYPES[0], // the costliest default: treat it as a real question
78
+ typed: Boolean(declared),
79
+ text: body.trim(),
80
+ });
81
+ }
82
+ return points;
83
+ }
84
+
85
+ /**
86
+ * @param {string} markdown a written spec file
87
+ * @returns {{ok: boolean, missing: string[], empty: string[],
88
+ * openPoints: Array<{type: string, typed: boolean, text: string}>,
89
+ * untyped: string[], unchecked: string[]}}
90
+ */
91
+ export function specCompleteness(markdown) {
92
+ const lines = String(markdown ?? '').split('\n');
93
+ const sections = new Map(); // family -> body lines
94
+
95
+ let current = null;
96
+ for (const line of lines) {
97
+ const h = line.match(/^##\s+(.*)$/);
98
+ if (h) {
99
+ const fam = familyOf(h[1]);
100
+ current = fam;
101
+ if (fam && !sections.has(fam)) sections.set(fam, []);
102
+ continue;
103
+ }
104
+ if (current && sections.has(current)) sections.get(current).push(line);
105
+ }
106
+
107
+ const families = Object.keys(FAMILIES);
108
+ const missing = families.filter((f) => !sections.has(f));
109
+ const empty = families.filter((f) => sections.has(f) && !sections.get(f).some(isSubstantive));
110
+
111
+ const openPoints = sections.has('open') ? parseOpenPoints(sections.get('open')) : [];
112
+ const untyped = openPoints.filter((p) => !p.typed).map((p) => p.text);
113
+
114
+ return {
115
+ ok: missing.length === 0 && empty.length === 0,
116
+ missing,
117
+ empty,
118
+ openPoints,
119
+ untyped,
120
+ unchecked: UNCHECKED,
121
+ };
122
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // Runs the `specify` exit gate over written specs. Report-only: it never edits a spec.
3
+ //
4
+ // Not wired into `prepublishOnly`, and that is deliberate: specs live under 02-DOCS, which is never
5
+ // tracked (principle 9), so a publish gate depending on them would fail in any clean clone. This is
6
+ // the phase's own instrument, and the author's.
7
+ import { readFileSync, readdirSync } from 'node:fs';
8
+ import { join, dirname, basename } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { specCompleteness } from './lib/spec-gate.js';
11
+
12
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
13
+ const DEFAULT_DIR = join(ROOT, '02-DOCS', 'wiki', 'sdd', 'specs');
14
+
15
+ function defaultTargets() {
16
+ try {
17
+ return readdirSync(DEFAULT_DIR)
18
+ .filter((f) => f.endsWith('.md') && !f.endsWith('.plan.md'))
19
+ .map((f) => join(DEFAULT_DIR, f));
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ function main() {
26
+ const args = process.argv.slice(2);
27
+ const targets = args.length ? args : defaultTargets();
28
+ if (!targets.length) {
29
+ console.log('no specs found — pass a path, or write one to 02-DOCS/wiki/sdd/specs/');
30
+ return;
31
+ }
32
+
33
+ let failed = 0;
34
+ for (const path of targets) {
35
+ const r = specCompleteness(readFileSync(path, 'utf8'));
36
+ const name = basename(path);
37
+ if (r.ok) {
38
+ const typed = r.openPoints.length - r.untyped.length;
39
+ console.log(`PASS ${name} — ${r.openPoints.length} open point(s), ${typed} typed`);
40
+ if (r.untyped.length) {
41
+ console.log(` untyped (treated as open questions): ${r.untyped.length}`);
42
+ }
43
+ } else {
44
+ failed += 1;
45
+ console.log(`FAIL ${name}`);
46
+ if (r.missing.length) console.log(` missing section(s): ${r.missing.join(', ')}`);
47
+ if (r.empty.length) console.log(` section(s) with no content: ${r.empty.join(', ')}`);
48
+ }
49
+ }
50
+
51
+ // A green here is narrower than the rule it enforces, so it says so every run rather than
52
+ // letting the reader assume the gate covered more than it did.
53
+ console.log(`\n${targets.length - failed}/${targets.length} pass. Not checked by this gate:`);
54
+ for (const u of specCompleteness('').unchecked) console.log(` - ${u}`);
55
+ if (failed) process.exit(1);
56
+ }
57
+
58
+ main();
@@ -28,12 +28,36 @@ This is the fourth phase of the rsc SDD chain (`constitution` → `specify` →
28
28
 
29
29
  Clarify never works blind. Before asking a single question, load three things:
30
30
 
31
- 1. **The spec.** Read the target spec under `02-DOCS/wiki/sdd/specs/<slug>.md` end to end. If the path wasn't given, find the most recently touched spec or ask which one.
31
+ 1. **The spec.** Read the target spec under `02-DOCS/wiki/sdd/specs/<slug>.md` end to end. If the path wasn't given, find the most recently touched spec or ask which one. Its *Points to clarify* is a **typed** handoff, not a question list — read the types before you plan a single question (below).
32
32
  2. **The constitution.** Read `02-DOCS/wiki/sdd/constitution.md` if it exists. Its principles (stack canon, quality bars, conventions) resolve a surprising number of "ambiguities" without bothering the user — if the constitution already fixes the auth method or the data region, that's answered, not open.
33
33
  3. **The harness profile.** `02-DOCS/wiki/harness/user-profile.md`, for the dial above.
34
34
 
35
35
  Citing what you read ("checked the constitution — auth is already fixed to OAuth, so that's not an open question") shows your work and prevents re-litigating settled decisions.
36
36
 
37
+ ## The typed handoff — what `specify` already decided
38
+
39
+ *Points to clarify* holds four different objects, and each one gets a different action from you.
40
+ Treating all four as questions is how clarify re-asks what was already decided and disturbs what was
41
+ deliberately deferred:
42
+
43
+ | Type in the spec | What it means | Your action |
44
+ | --- | --- | --- |
45
+ | **pregunta abierta** | Formulable, unanswered | Ask it — this is your queue |
46
+ | **suposición tomada** | `specify` decided it; the basis is written | **Validate**, don't re-ask: state the assumption and its basis back, and ask only whether it still holds |
47
+ | **decisión diferida** | Sharp, out of this cycle on purpose | Leave it. Do not reopen scope the author closed |
48
+ | **área no formulable** | Known to be coming, not yet phrasable | Note it. If your pass sharpens it into a real question, it graduates — say that it did |
49
+
50
+ **Declare what you did with each entry.** Close the pass with one line per point: asked, validated
51
+ (held / broke), left deferred, or graduated. An entry you silently dropped is the gap that comes back
52
+ at implementation time.
53
+
54
+ **An untyped entry** (an older spec, or a hurried one) is read as **pregunta abierta** — the
55
+ costliest reading, so nothing gets skipped by accident. Type it as you go, so the spec improves on
56
+ the way through.
57
+
58
+ Sharpness, not difficulty, is what separates a question from an unformulable area: *can you state it
59
+ precisely now?* Not *can you answer it?*
60
+
37
61
  ## The ambiguity taxonomy — where specs hide their gaps
38
62
 
39
63
  Scan the spec against these categories. Most real gaps fall into one of them; walking the list is how you find the ones the author didn't think to write down.
@@ -111,7 +135,7 @@ After baking back, the spec line becomes a bounded, testable behavior with accep
111
135
 
112
136
  ## Exit gate
113
137
 
114
- The gate is passed when the spec, constitution and profile were all read (settled questions cited, not re-asked); all ten taxonomy categories were considered; only the build-changing gaps were put to the user, as dial-sized decisions with recommendations; every answer is baked into the spec body with observable acceptance criteria, logged under `## Clarifications` and bounded under `## Out of scope`; and the final re-read opened no new gap.
138
+ The gate is passed when the spec, constitution and profile were all read (settled questions cited, not re-asked); **every typed point in the handoff has its declared outcome** (asked / validated / left deferred / graduated), with nothing silently dropped; all ten taxonomy categories were considered; only the build-changing gaps were put to the user, as dial-sized decisions with recommendations; every answer is baked into the spec body with observable acceptance criteria, logged under `## Clarifications` and bounded under `## Out of scope`; and the final re-read opened no new gap.
115
139
 
116
140
  ## Result envelope
117
141
 
@@ -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
 
@@ -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 grammar
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
- Good fix(auth): reject expired refresh tokens
82
- Good feat(api): add /v2/search endpoint with cursor paging
83
- Good refactor(parser): extract token scanner, no behavior change
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 | conventional `type(scope): imperative subject` |
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 is conventional; `wip`/scratch commits squashed away.
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.