@next-friday/create-app 9.0.0 → 9.1.1

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.
Files changed (57) hide show
  1. package/package.json +5 -2
  2. package/templates/base/.agents/review.md +1 -1
  3. package/templates/base/.agents/skills/sdlc/SKILL.md +14 -12
  4. package/templates/base/.agents/skills/sdlc/playbooks/build.md +10 -9
  5. package/templates/base/.agents/skills/sdlc/playbooks/deploy.md +9 -0
  6. package/templates/base/.agents/skills/sdlc/playbooks/design.md +8 -5
  7. package/templates/base/.agents/skills/sdlc/playbooks/maintain.md +21 -0
  8. package/templates/base/.agents/skills/sdlc/playbooks/open-pr.md +4 -4
  9. package/templates/base/.agents/skills/sdlc/playbooks/plan.md +5 -5
  10. package/templates/base/.agents/skills/sdlc/playbooks/review.md +12 -5
  11. package/templates/base/.agents/skills/sdlc-help/SKILL.md +4 -5
  12. package/templates/base/.claude/skills/sdlc/SKILL.md +14 -12
  13. package/templates/base/.claude/skills/sdlc/playbooks/build.md +10 -9
  14. package/templates/base/.claude/skills/sdlc/playbooks/deploy.md +9 -0
  15. package/templates/base/.claude/skills/sdlc/playbooks/design.md +8 -5
  16. package/templates/base/.claude/skills/sdlc/playbooks/maintain.md +21 -0
  17. package/templates/base/.claude/skills/sdlc/playbooks/open-pr.md +4 -4
  18. package/templates/base/.claude/skills/sdlc/playbooks/plan.md +5 -5
  19. package/templates/base/.claude/skills/sdlc/playbooks/review.md +12 -5
  20. package/templates/base/.claude/skills/sdlc-help/SKILL.md +4 -5
  21. package/templates/base/.cursor/skills/sdlc/SKILL.md +14 -12
  22. package/templates/base/.cursor/skills/sdlc/playbooks/build.md +10 -9
  23. package/templates/base/.cursor/skills/sdlc/playbooks/deploy.md +9 -0
  24. package/templates/base/.cursor/skills/sdlc/playbooks/design.md +8 -5
  25. package/templates/base/.cursor/skills/sdlc/playbooks/maintain.md +21 -0
  26. package/templates/base/.cursor/skills/sdlc/playbooks/open-pr.md +4 -4
  27. package/templates/base/.cursor/skills/sdlc/playbooks/plan.md +5 -5
  28. package/templates/base/.cursor/skills/sdlc/playbooks/review.md +12 -5
  29. package/templates/base/.cursor/skills/sdlc-help/SKILL.md +4 -5
  30. package/templates/base/.github/ISSUE_TEMPLATE/documentation.yml +25 -0
  31. package/templates/base/REVIEW.md +1 -1
  32. package/templates/base/SECURITY.md +1 -1
  33. package/templates/base/SETUP.md +9 -13
  34. package/templates/base/bands.yaml +2 -3
  35. package/templates/base/intent/README.md +2 -4
  36. package/templates/base/lessons/README.md +5 -0
  37. package/templates/base/scripts/check-intent.ts +391 -0
  38. package/templates/nestjs/.github/workflows/ci.yml +2 -0
  39. package/templates/nestjs/AGENTS.md +9 -17
  40. package/templates/nestjs/CONTRIBUTING.md +1 -0
  41. package/templates/nestjs/package.json +2 -1
  42. package/templates/nextjs/.github/workflows/ci.yml +2 -0
  43. package/templates/nextjs/AGENTS.md +9 -17
  44. package/templates/nextjs/CONTRIBUTING.md +1 -0
  45. package/templates/nextjs/package.json +2 -1
  46. package/templates/nodejs/.github/workflows/ci.yml +2 -0
  47. package/templates/nodejs/AGENTS.md +8 -16
  48. package/templates/nodejs/CONTRIBUTING.md +1 -0
  49. package/templates/nodejs/package.json +2 -1
  50. package/templates/payload/.github/workflows/ci.yml +2 -0
  51. package/templates/payload/AGENTS.md +10 -18
  52. package/templates/payload/CONTRIBUTING.md +1 -0
  53. package/templates/payload/package.json +2 -1
  54. package/templates/turborepo/.github/workflows/ci.yml +2 -0
  55. package/templates/turborepo/AGENTS.md +10 -18
  56. package/templates/turborepo/CONTRIBUTING.md +1 -0
  57. package/templates/turborepo/package.json +2 -1
@@ -0,0 +1,391 @@
1
+ import {existsSync, readFileSync} from "node:fs";
2
+
3
+ type Problem = {
4
+ line: number;
5
+ message: string;
6
+ };
7
+
8
+ type Heading = {
9
+ line: number;
10
+ text: string;
11
+ };
12
+
13
+ type SelfTestCase = {
14
+ expectedMessagePart?: string;
15
+ name: string;
16
+ text: string;
17
+ };
18
+
19
+ const problemHeading = "## Problem";
20
+ const constraintsHeading = "## Constraints";
21
+ const openQuestionsHeading = "## Open questions";
22
+
23
+ const requiredHeadings = [
24
+ problemHeading,
25
+ "## Proposed outcome",
26
+ "## Affected users and systems",
27
+ constraintsHeading,
28
+ openQuestionsHeading,
29
+ ];
30
+
31
+ const titlePattern = /^# Intent: \S/;
32
+ const statusPattern = /Status: (?:draft|accepted|rejected)\.$/;
33
+
34
+ /**
35
+ * Blank every line of a fenced code block, keeping line numbers.
36
+ * @param text Intent content with LF line endings.
37
+ * @returns The content with fenced lines emptied.
38
+ */
39
+ function maskFencedBlocks(text: string): string {
40
+ let isFenced = false;
41
+
42
+ return text
43
+ .split("\n")
44
+ .map(line => {
45
+ const isFence = /^\s*(?:```|~~~)/.test(line);
46
+ const isMasked = isFenced || isFence;
47
+
48
+ isFenced = isFence ? !isFenced : isFenced;
49
+
50
+ return isMasked ? "" : line;
51
+ })
52
+ .join("\n");
53
+ }
54
+
55
+ /**
56
+ * Find a first line that is not `# Intent: <title>`.
57
+ * @param lines Intent lines.
58
+ * @returns At most one problem.
59
+ */
60
+ function findTitleProblems(lines: string[]): Problem[] {
61
+ return titlePattern.test(lines.at(0) ?? "")
62
+ ? []
63
+ : [
64
+ {
65
+ message:
66
+ 'line 1 must be "# Intent: <title>" with a non-empty title; write the change\'s name after "# Intent: "',
67
+ line: 1,
68
+ },
69
+ ];
70
+ }
71
+
72
+ /**
73
+ * Find a missing or malformed author line.
74
+ * @param lines Intent lines.
75
+ * @returns At most one problem.
76
+ */
77
+ function findAuthorProblems(lines: string[]): Problem[] {
78
+ const authorLines = lines
79
+ .map((text, index) => ({
80
+ line: index + 1,
81
+ text: text.trimEnd(),
82
+ }))
83
+ .filter(({text}) => text.startsWith("Author: "));
84
+
85
+ if (authorLines.length > 1) {
86
+ return [
87
+ {
88
+ message: 'only one author line is allowed; remove the extra "Author: " line',
89
+ line: authorLines.at(1)?.line ?? 1,
90
+ },
91
+ ];
92
+ }
93
+
94
+ const authorLine = authorLines.at(0);
95
+
96
+ return authorLine !== undefined && statusPattern.test(authorLine.text)
97
+ ? []
98
+ : [
99
+ {
100
+ message:
101
+ 'an author line must start with "Author: " and end with "Status: draft.", "Status: accepted." or "Status: rejected."; write "Author: <name> (<team>). Status: draft."',
102
+ line: authorLine?.line ?? 1,
103
+ },
104
+ ];
105
+ }
106
+
107
+ /**
108
+ * Quote each heading and join them for a message.
109
+ * @param headings Heading texts.
110
+ * @returns The quoted list, or "none" when there is no heading.
111
+ */
112
+ function quoteHeadings(headings: string[]): string {
113
+ return headings.length === 0 ? "none" : headings.map(heading => `"${heading}"`).join(", ");
114
+ }
115
+
116
+ /**
117
+ * Find level-2 headings that differ from the template's, in count, spelling, or order.
118
+ * @param headings The level-2 headings with their line numbers.
119
+ * @returns At most one problem.
120
+ */
121
+ function findHeadingProblems(headings: Heading[]): Problem[] {
122
+ const texts = headings.map(({text}) => text);
123
+ const expected = quoteHeadings(requiredHeadings);
124
+
125
+ return texts.join("\n") === requiredHeadings.join("\n")
126
+ ? []
127
+ : [
128
+ {
129
+ line: headings.at(0)?.line ?? 1,
130
+ message: `the level-2 headings must be exactly, in order: ${expected}; found ${quoteHeadings(texts)}`,
131
+ },
132
+ ];
133
+ }
134
+
135
+ /**
136
+ * Find sections without text.
137
+ * @param lines Intent lines.
138
+ * @param headings The level-2 headings with their line numbers.
139
+ * @returns One problem per empty section.
140
+ */
141
+ function findEmptySectionProblems(lines: string[], headings: Heading[]): Problem[] {
142
+ return headings
143
+ .filter(({line}, index) => {
144
+ const end = headings.at(index + 1)?.line ?? lines.length + 1;
145
+
146
+ return (
147
+ lines
148
+ .slice(line, end - 1)
149
+ .join("\n")
150
+ .trim() === ""
151
+ );
152
+ })
153
+ .map(({line, text}) => ({
154
+ line,
155
+ message: `the section "${text}" is empty; write its text or its open question`,
156
+ }));
157
+ }
158
+
159
+ /**
160
+ * Find a Problem section that holds no digit outside fenced code blocks.
161
+ * @param lines Intent lines.
162
+ * @param maskedLines Intent lines with fenced code blocks emptied.
163
+ * @param headings The level-2 headings with their line numbers.
164
+ * @returns At most one problem.
165
+ */
166
+ function findProblemNumberProblems(
167
+ lines: string[],
168
+ maskedLines: string[],
169
+ headings: Heading[],
170
+ ): Problem[] {
171
+ const index = headings.findIndex(({text}) => text === problemHeading);
172
+ const start = headings.at(index)?.line;
173
+
174
+ if (start === undefined) {
175
+ return [];
176
+ }
177
+
178
+ const end = headings.at(index + 1)?.line ?? lines.length + 1;
179
+ const body = lines.slice(start, end - 1).join("\n");
180
+ const unfencedBody = maskedLines.slice(start, end - 1).join("\n");
181
+
182
+ return body.trim() === "" || /\d/.test(unfencedBody)
183
+ ? []
184
+ : [
185
+ {
186
+ line: start,
187
+ message: `the section "${problemHeading}" has no number; state the measured value, its source, and the time`,
188
+ },
189
+ ];
190
+ }
191
+
192
+ /**
193
+ * Check one intent against the template in `.agents/skills/sdlc/playbooks/plan.md`.
194
+ * @param text Intent content.
195
+ * @returns Every problem found.
196
+ */
197
+ function checkIntent(text: string): Problem[] {
198
+ const lines = text
199
+ .replace(/^\u{FEFF}/u, "")
200
+ .replaceAll("\r\n", "\n")
201
+ .split("\n");
202
+
203
+ const maskedLines = maskFencedBlocks(lines.join("\n")).split("\n");
204
+
205
+ const headings = maskedLines
206
+ .map((line, index) => ({
207
+ line: index + 1,
208
+ text: line.trimEnd(),
209
+ }))
210
+ .filter(({text: heading}) => heading.startsWith("## "));
211
+
212
+ return [
213
+ ...findTitleProblems(maskedLines),
214
+ ...findAuthorProblems(maskedLines),
215
+ ...findHeadingProblems(headings),
216
+ ...findEmptySectionProblems(lines, headings),
217
+ ...findProblemNumberProblems(lines, maskedLines, headings),
218
+ ];
219
+ }
220
+
221
+ const titleLine = "# Intent: Health check answers 200";
222
+ const authorLine = "Author: maintain job (alert 1). Status: draft.";
223
+ const problemText = "GET /health returned 500 on 12 of 12 requests, from the alert log.";
224
+ const constraintsSection = `${constraintsHeading}\n\nNone.\n\n`;
225
+ const headingsPart = "level-2 headings";
226
+ const authorPart = "author line";
227
+ const statusDraft = "Status: draft.";
228
+
229
+ const validIntent = [
230
+ titleLine,
231
+ "",
232
+ authorLine,
233
+ "",
234
+ problemHeading,
235
+ "",
236
+ problemText,
237
+ "",
238
+ "## Proposed outcome",
239
+ "",
240
+ "GET /health answers 200.",
241
+ "",
242
+ "## Affected users and systems",
243
+ "",
244
+ "The load balancer.",
245
+ "",
246
+ constraintsHeading,
247
+ "",
248
+ "None.",
249
+ "",
250
+ openQuestionsHeading,
251
+ "",
252
+ "Which commit broke it?",
253
+ "",
254
+ ].join("\n");
255
+
256
+ const selfTestCases: SelfTestCase[] = [
257
+ {
258
+ text: validIntent,
259
+ name: "a complete intent",
260
+ },
261
+ {
262
+ name: "CRLF line endings",
263
+ text: validIntent.replaceAll("\n", "\r\n"),
264
+ },
265
+ {
266
+ name: "headings inside a fenced block are ignored",
267
+ text: validIntent.replace("None.", "```\n## Extra\n```"),
268
+ },
269
+ {
270
+ expectedMessagePart: "line 1",
271
+ name: "title is missing",
272
+ text: validIntent.replace(titleLine, "# Health check"),
273
+ },
274
+ {
275
+ expectedMessagePart: "line 1",
276
+ name: "title is empty",
277
+ text: validIntent.replace(titleLine, "# Intent: "),
278
+ },
279
+ {
280
+ expectedMessagePart: authorPart,
281
+ name: "author line is missing",
282
+ text: validIntent.replace(`${authorLine}\n`, ""),
283
+ },
284
+ {
285
+ expectedMessagePart: authorPart,
286
+ name: "status is not a known value",
287
+ text: validIntent.replace(statusDraft, "Status: open."),
288
+ },
289
+ {
290
+ expectedMessagePart: authorPart,
291
+ name: "status has no final period",
292
+ text: validIntent.replace(statusDraft, "Status: draft"),
293
+ },
294
+ {
295
+ expectedMessagePart: headingsPart,
296
+ name: "a heading is missing",
297
+ text: validIntent.replace(constraintsSection, ""),
298
+ },
299
+ {
300
+ expectedMessagePart: headingsPart,
301
+ name: "a heading is misspelled",
302
+ text: validIntent.replace(constraintsHeading, "## Constraint"),
303
+ },
304
+ {
305
+ expectedMessagePart: headingsPart,
306
+ name: "headings are out of order",
307
+ text: [
308
+ validIntent.split("\n## ", 1).at(0),
309
+ ...validIntent.split("\n## ").slice(1).toReversed(),
310
+ ].join("\n## "),
311
+ },
312
+ {
313
+ expectedMessagePart: headingsPart,
314
+ name: "an extra heading",
315
+ text: `${validIntent}\n## Extra\n\nText.\n`,
316
+ },
317
+ {
318
+ name: "a BOM before the title",
319
+ text: `\u{FEFF}${validIntent}`,
320
+ },
321
+ {
322
+ expectedMessagePart: authorPart,
323
+ name: "status is not at the end of the author line",
324
+ text: validIntent.replace(statusDraft, "Status: draft. Reviewed."),
325
+ },
326
+ {
327
+ expectedMessagePart: authorPart,
328
+ name: "a second author line",
329
+ text: `${validIntent}\nAuthor: second. Status: draft.\n`,
330
+ },
331
+ {
332
+ expectedMessagePart: "is empty",
333
+ name: "a section is empty",
334
+ text: validIntent.replace("None.", ""),
335
+ },
336
+ {
337
+ expectedMessagePart: "has no number",
338
+ name: "problem has no digit",
339
+ text: validIntent.replace(problemText, "GET /health fails."),
340
+ },
341
+ {
342
+ expectedMessagePart: "has no number",
343
+ name: "problem has a digit only inside a fenced block",
344
+ text: validIntent.replace(problemText, "GET /health fails.\n\n```\n500\n```"),
345
+ },
346
+ ];
347
+
348
+ const selfTestFailures = selfTestCases
349
+ .filter(({expectedMessagePart, text}) => {
350
+ const messages = checkIntent(text).map(problem => problem.message);
351
+
352
+ return expectedMessagePart === undefined
353
+ ? messages.length > 0
354
+ : messages.length !== 1 || !messages.at(0)?.includes(expectedMessagePart);
355
+ })
356
+ .map(({name}) => name);
357
+
358
+ const intentPaths = process.argv.slice(2);
359
+
360
+ if (selfTestFailures.length > 0) {
361
+ const failureLines = selfTestFailures.map(name => `- ${name}`);
362
+
363
+ process.stderr.write(`Intent self-tests failed:\n${failureLines.join("\n")}\n`);
364
+ process.exitCode = 1;
365
+ } else if (intentPaths.length === 0) {
366
+ process.stdout.write(`Ran ${selfTestCases.length} intent self-tests.\n`);
367
+ } else {
368
+ const missingLines = intentPaths
369
+ .filter(intentPath => !existsSync(intentPath))
370
+ .map(
371
+ intentPath =>
372
+ `${intentPath}:1: the file does not exist; pass the path of an existing intent.md`,
373
+ );
374
+
375
+ const checkedLines = intentPaths
376
+ .filter(intentPath => existsSync(intentPath))
377
+ .flatMap(intentPath =>
378
+ checkIntent(readFileSync(intentPath, "utf8")).map(
379
+ ({line, message}) => `${intentPath}:${line}: ${message}`,
380
+ ),
381
+ );
382
+
383
+ const problemLines = [...missingLines, ...checkedLines];
384
+
385
+ if (problemLines.length > 0) {
386
+ process.stderr.write(`${problemLines.join("\n")}\n`);
387
+ process.exitCode = 1;
388
+ } else {
389
+ process.stdout.write(`Checked ${intentPaths.length} intents.\n`);
390
+ }
391
+ }
@@ -81,6 +81,8 @@ jobs:
81
81
  run: pnpm gates:check
82
82
  - name: Check human gates
83
83
  run: pnpm human-gates:check
84
+ - name: Self-test the intent checker
85
+ run: pnpm intent:check
84
86
  - name: Check lint
85
87
  run: pnpm lint:check
86
88
  - name: Check formatting
@@ -21,7 +21,6 @@ NestJS HTTP API in TypeScript on pnpm and Node.js `>=24.15.0 <25`.
21
21
  - Lint: `pnpm verify:static` (ESLint allows zero warnings; prints `✔ no dependency violations found`)
22
22
 
23
23
  Run all three before reporting any task complete, and paste the output. `pnpm verify` runs all three.
24
- If a test fails, fix the code, not the test.
25
24
 
26
25
  ## Architecture
27
26
 
@@ -30,24 +29,17 @@ If a test fails, fix the code, not the test.
30
29
 
31
30
  ## Conventions
32
31
 
33
- 1. **Follow `.agents/rules/`.** `.agents/rules.json` gives each rule's scope; Claude Code and Cursor load them. After changing either, or a skill or subagent in `.agents/skills/` or `.agents/agents/`, run `pnpm rules:sync`; `pnpm check` fails on stale copies.
34
- 2. **Put unit tests next to the code as `src/**/*.test.ts` and end-to-end tests in `test/` as `*.e2e.test.ts`.** `vitest.config.ts` and `vitest.e2e.config.ts` collect only those patterns.
35
- 3. **Read configuration from environment variables and fail when one is missing**, as `parsePort` does. Document each variable in `.env.example`.
36
- 4. **Log through Nest's `Logger`.** It goes to the `ConsoleLogger` set in `src/main.ts`.
37
- 5. **Start from the accepted spec, in plan mode.** Claude Code starts every session in plan mode (`.claude/settings.json`); in Cursor, switch to Plan Mode before the first edit. Give the plan the change's `intent/<change>/intent.md` and `spec.md`, and have it name the files that change, the order of the work, and the tests that prove it. Commit the approved plan as `intent/<change>/plan.md`. When implementation departs from the plan, update `plan.md` in the same commit.
38
- 6. **Start a bug fix with a failing test.** Reproduce the bug as a test, run it, and confirm it fails for the reason you expect. Commit that test. Then create `.git/bug-fix-in-progress`, so `.agents/hooks/agent-test-edit-gate.sh` blocks test edits, and make the test pass without editing it. Delete `.git/bug-fix-in-progress` when the fix is done.
39
- 7. **Start work only when a person asks or a trigger runs it, and stop only at the human gates, before an irreversible action, or for a decision only a person can make.** A question or a statement gets an answer, not a file, a commit, or a pull request. When a message reports a problem without asking for work, answer in a few lines and end with the message that would start the work: `/sdlc` followed by their own words. Once a task starts, carry it to the next gate without asking; `.agents/rules/agent-owned-work.md` lists the gates.
40
- 8. **Branch from the latest `origin/main`, and open the pull request ready for review.** Name the branch `<type>/<short-description>` and fill the body from `.github/pull_request_template.md`. `.github/workflows/ci.yml` runs full verification only on a pull request that is not a draft. Drive it until it is green and waits only on code owner approval, as `.agents/skills/sdlc/playbooks/review.md` describes. Rebase and `git push --force-with-lease` only branches you own. Never merge a pull request: a person merges it with `--squash` after code owner approval, bottom-up for a stack of pull requests.
41
- 9. **Write replies a person can check.** Reply in the language the person wrote in. Write paths from the project root, such as `README.md:12`, never from the machine's root. State as a fact only what you opened or ran in this turn, and say what you ran; mark anything else as inferred or not checked. Answer what was asked and stop: leave out tools, services, and steps the request does not need, and never hand the person a check you could run. Send nothing outside the session, such as a message to another session or a service, unless a task step says to.
32
+ 1. **Put unit tests next to the code as `src/**/*.test.ts` and end-to-end tests in `test/` as `*.e2e.test.ts`.** `vitest.config.ts` and `vitest.e2e.config.ts` collect only those patterns.
33
+ 2. **Read configuration from environment variables and fail when one is missing**, as `parsePort` does. Document each variable in `.env.example`.
34
+ 3. **Log through Nest's `Logger`.** It goes to the `ConsoleLogger` set in `src/main.ts`.
35
+ 4. **Start from the accepted spec, in plan mode** (Cursor: Plan Mode). Give the plan the change's `intent/<change>/intent.md` and `spec.md`. Commit the approved plan as `intent/<change>/plan.md` with an `Accepted by:` line naming the engineer, as `.agents/skills/sdlc/playbooks/build.md` describes.
36
+ 5. **Start a bug fix with a failing test.** Reproduce the bug as a test, confirm it fails for the reason you expect, and commit it. Then create `.git/bug-fix-in-progress`, so `.agents/hooks/agent-test-edit-gate.sh` blocks test edits, make the test pass without editing it, and delete `.git/bug-fix-in-progress`.
37
+ 6. **Do not start work a person did not ask for.** When a message reports a problem without asking for work, answer in a few lines and end with the message that would start it: `/sdlc` followed by their own words.
38
+ 7. **Branch from the latest `origin/main`, and open the pull request ready for review.** Name the branch `<type>/<short-description>` and fill the body from `.github/pull_request_template.md`. `.github/workflows/ci.yml` runs full verification only on a pull request that is not a draft. Never merge a pull request: a person merges it with `--squash` after code owner approval, bottom-up for a stack of pull requests.
39
+ 8. **Write replies a person can check.** Reply in the language the person wrote in. Write paths from the project root, such as `README.md:12`, never from the machine's root. State as a fact only what you opened or ran in this turn, and say what you ran; mark anything else as inferred or not checked. Answer what was asked and stop: leave out tools, services, and steps the request does not need, and never hand the person a check you could run. Send nothing outside the session, such as a message to another session or a service, unless a task step says to.
42
40
 
43
41
  ## Things Claude gets wrong
44
42
 
45
43
  When Claude makes a mistake twice, add the correction here.
46
44
 
47
- - **Agent shell gate**: `.agents/hooks/agent-shell-gate.sh` blocks a shell command that writes to a protected path such as `.agents`, `.cursor`, `.claude/rules`, or `.husky`. It also blocks merging or approving a pull request, approving a deployment, and gh aliases, and asks the person before an irreversible git command such as a force push or a hard reset. It is a convenience, not a security boundary: CI and code-owner review are the boundary.
48
- - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` (`gh workflow run` or a workflow dispatch with `environment=prod`), a Railway CLI command that names `prod` with `-e`, `--environment`, or `railway environment`, and a release tag push, unless a release manager started the session with `RELEASE_APPROVAL` set. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it. It is a convenience, not a security boundary: the `prod` environment's required reviewers and Railway credentials are the boundary.
49
-
50
- ## Related documentation
51
-
52
- - [README.md](README.md): scripts, container, deploy
53
- - [CONTRIBUTING.md](CONTRIBUTING.md): prerequisites, Git hooks, pull requests
45
+ - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod`, runs a Railway CLI command that names `prod`, or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it.
@@ -50,6 +50,7 @@ We use trunk-based development. `main` is the only long-lived branch, and every
50
50
  - Branch from `main` and merge back within a day or two. Do not create `develop` or release branches.
51
51
  - Merge unfinished work behind a feature flag or keep it unreachable instead of holding a long-lived branch.
52
52
  - Pull requests are squash-merged, so the pull request title becomes the commit on `main`.
53
+ - After a pull request that only reformats code merges, the next pull request lists its commit on `main` in `.git-blame-ignore-revs` at the repository root, creating the file the first time, with a `#` comment that names the change, as [GitHub Docs](https://docs.github.com/en/repositories/working-with-files/using-files/viewing-and-understanding-files) shows. GitHub then hides that commit in the blame view.
53
54
  - Write commit messages in the format that `@next-friday/commitlint-config-friday` defines. Pull request titles use `<type>(<scope>): <subject>`, which `.github/workflows/validate-pr-title.yml` checks.
54
55
  - Keep each pull request focused on one cohesive change.
55
56
  - Describe observable endpoint, request or response, or configuration effects.
@@ -23,6 +23,7 @@
23
23
  "format:check": "prettier --check --cache . && editorconfig-checker",
24
24
  "gates:check": "node scripts/check-agent-gates.ts",
25
25
  "human-gates:check": "node scripts/check-human-gates.ts",
26
+ "intent:check": "node scripts/check-intent.ts",
26
27
  "knip": "knip",
27
28
  "lint": "eslint . --fix --cache --max-warnings=0 --no-inline-config",
28
29
  "lint:check": "eslint . --cache --max-warnings=0 --no-inline-config",
@@ -44,7 +45,7 @@
44
45
  "test:watch": "vitest",
45
46
  "typecheck": "tsc --noEmit -p tsconfig.json",
46
47
  "verify": "pnpm verify:static && pnpm test:coverage && pnpm build",
47
- "verify:static": "pnpm suppressions:check && pnpm rules:check && pnpm gates:check && pnpm human-gates:check && pnpm lint:check && pnpm format:check && pnpm sort-package-json:check && pnpm peers:check && pnpm typecheck && pnpm knip && pnpm depcruise"
48
+ "verify:static": "pnpm suppressions:check && pnpm rules:check && pnpm gates:check && pnpm human-gates:check && pnpm intent:check && pnpm lint:check && pnpm format:check && pnpm sort-package-json:check && pnpm peers:check && pnpm typecheck && pnpm knip && pnpm depcruise"
48
49
  },
49
50
  "dependencies": {
50
51
  "@nestjs/common": "12.3.2",
@@ -81,6 +81,8 @@ jobs:
81
81
  run: pnpm gates:check
82
82
  - name: Check human gates
83
83
  run: pnpm human-gates:check
84
+ - name: Self-test the intent checker
85
+ run: pnpm intent:check
84
86
  - name: Check lint
85
87
  run: pnpm lint:check
86
88
  - name: Check formatting
@@ -21,7 +21,6 @@ Next.js App Router web app with React and Tailwind CSS, in TypeScript on pnpm an
21
21
  - Lint: `pnpm verify:static` (ESLint allows zero warnings; prints `✔ no dependency violations found`)
22
22
 
23
23
  Run all three before reporting any task complete, and paste the output. `pnpm verify` runs all three.
24
- If a test fails, fix the code, not the test.
25
24
 
26
25
  ## Architecture
27
26
 
@@ -30,28 +29,21 @@ If a test fails, fix the code, not the test.
30
29
 
31
30
  ## Conventions
32
31
 
33
- 1. **Follow `.agents/rules/`.** `.agents/rules.json` gives each rule's scope; Claude Code and Cursor load them. After changing either, or a skill or subagent in `.agents/skills/` or `.agents/agents/`, run `pnpm rules:sync`; `pnpm check` fails on stale copies.
34
- 2. **Put unit tests next to the code as `app/**/*.test.{ts,tsx}` and Playwright end-to-end tests in `test/` as `*.e2e.test.ts`.** Vitest collects only the first pattern.
35
- 3. **Component tests need a DOM test environment, and Vitest runs in Node.js.** Add that environment with the first component test.
36
- 4. **Read configuration from environment variables and fail when one is missing.** Document each variable in `.env.example`.
37
- 5. **Start from the accepted spec, in plan mode.** Claude Code starts every session in plan mode (`.claude/settings.json`); in Cursor, switch to Plan Mode before the first edit. Give the plan the change's `intent/<change>/intent.md` and `spec.md`, and have it name the files that change, the order of the work, and the tests that prove it. Commit the approved plan as `intent/<change>/plan.md`. When implementation departs from the plan, update `plan.md` in the same commit.
38
- 6. **Start a bug fix with a failing test.** Reproduce the bug as a test, run it, and confirm it fails for the reason you expect. Commit that test. Then create `.git/bug-fix-in-progress`, so `.agents/hooks/agent-test-edit-gate.sh` blocks test edits, and make the test pass without editing it. Delete `.git/bug-fix-in-progress` when the fix is done.
39
- 7. **Start work only when a person asks or a trigger runs it, and stop only at the human gates, before an irreversible action, or for a decision only a person can make.** A question or a statement gets an answer, not a file, a commit, or a pull request. When a message reports a problem without asking for work, answer in a few lines and end with the message that would start the work: `/sdlc` followed by their own words. Once a task starts, carry it to the next gate without asking; `.agents/rules/agent-owned-work.md` lists the gates.
40
- 8. **Branch from the latest `origin/main`, and open the pull request ready for review.** Name the branch `<type>/<short-description>` and fill the body from `.github/pull_request_template.md`. `.github/workflows/ci.yml` runs full verification only on a pull request that is not a draft. Drive it until it is green and waits only on code owner approval, as `.agents/skills/sdlc/playbooks/review.md` describes. Rebase and `git push --force-with-lease` only branches you own. Never merge a pull request: a person merges it with `--squash` after code owner approval, bottom-up for a stack of pull requests.
41
- 9. **Write replies a person can check.** Reply in the language the person wrote in. Write paths from the project root, such as `README.md:12`, never from the machine's root. State as a fact only what you opened or ran in this turn, and say what you ran; mark anything else as inferred or not checked. Answer what was asked and stop: leave out tools, services, and steps the request does not need, and never hand the person a check you could run. Send nothing outside the session, such as a message to another session or a service, unless a task step says to.
32
+ 1. **Put unit tests next to the code as `app/**/*.test.{ts,tsx}` and Playwright end-to-end tests in `test/` as `*.e2e.test.ts`.** Vitest collects only the first pattern.
33
+ 2. **Component tests need a DOM test environment, and Vitest runs in Node.js.** Add that environment with the first component test.
34
+ 3. **Read configuration from environment variables and fail when one is missing.** Document each variable in `.env.example`.
35
+ 4. **Start from the accepted spec, in plan mode** (Cursor: Plan Mode). Give the plan the change's `intent/<change>/intent.md` and `spec.md`. Commit the approved plan as `intent/<change>/plan.md` with an `Accepted by:` line naming the engineer, as `.agents/skills/sdlc/playbooks/build.md` describes.
36
+ 5. **Start a bug fix with a failing test.** Reproduce the bug as a test, confirm it fails for the reason you expect, and commit it. Then create `.git/bug-fix-in-progress`, so `.agents/hooks/agent-test-edit-gate.sh` blocks test edits, make the test pass without editing it, and delete `.git/bug-fix-in-progress`.
37
+ 6. **Do not start work a person did not ask for.** When a message reports a problem without asking for work, answer in a few lines and end with the message that would start it: `/sdlc` followed by their own words.
38
+ 7. **Branch from the latest `origin/main`, and open the pull request ready for review.** Name the branch `<type>/<short-description>` and fill the body from `.github/pull_request_template.md`. `.github/workflows/ci.yml` runs full verification only on a pull request that is not a draft. Never merge a pull request: a person merges it with `--squash` after code owner approval, bottom-up for a stack of pull requests.
39
+ 8. **Write replies a person can check.** Reply in the language the person wrote in. Write paths from the project root, such as `README.md:12`, never from the machine's root. State as a fact only what you opened or ran in this turn, and say what you ran; mark anything else as inferred or not checked. Answer what was asked and stop: leave out tools, services, and steps the request does not need, and never hand the person a check you could run. Send nothing outside the session, such as a message to another session or a service, unless a task step says to.
42
40
 
43
41
  ## Things Claude gets wrong
44
42
 
45
43
  When Claude makes a mistake twice, add the correction here.
46
44
 
47
45
  - **First end-to-end run**: run `pnpm exec playwright install chromium` once. `pnpm test:e2e` starts the development server unless one is already running.
48
- - **Agent shell gate**: `.agents/hooks/agent-shell-gate.sh` blocks a shell command that writes to a protected path such as `.agents`, `.cursor`, `.claude/rules`, or `.husky`. It also blocks merging or approving a pull request, approving a deployment, and gh aliases, and asks the person before an irreversible git command such as a force push or a hard reset. It is a convenience, not a security boundary: CI and code-owner review are the boundary.
49
- - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` (`gh workflow run` or a workflow dispatch with `environment=prod`), a Railway CLI command that names `prod` with `-e`, `--environment`, or `railway environment`, and a release tag push, unless a release manager started the session with `RELEASE_APPROVAL` set. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it. It is a convenience, not a security boundary: the `prod` environment's required reviewers and Railway credentials are the boundary.
50
-
51
- ## Related documentation
52
-
53
- - [README.md](README.md): scripts, container, deploy
54
- - [CONTRIBUTING.md](CONTRIBUTING.md): prerequisites, Git hooks, pull requests
46
+ - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod`, runs a Railway CLI command that names `prod`, or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Report what is ready to promote instead. When a gated command appears only inside a message, write the text to a file with your file-editing tool, not a shell heredoc, and pass it with `git commit -F <file>` or `gh pr create --body-file <file>`. The gate reads only shell commands, so an MCP tool or a Railway command without `-e` or `--environment` passes it.
55
47
 
56
48
  <!-- BEGIN:nextjs-agent-rules -->
57
49
 
@@ -50,6 +50,7 @@ We use trunk-based development. `main` is the only long-lived branch, and every
50
50
  - Branch from `main` and merge back within a day or two. Do not create `develop` or release branches.
51
51
  - Merge unfinished work behind a feature flag or keep it unreachable instead of holding a long-lived branch.
52
52
  - Pull requests are squash-merged, so the pull request title becomes the commit on `main`.
53
+ - After a pull request that only reformats code merges, the next pull request lists its commit on `main` in `.git-blame-ignore-revs` at the repository root, creating the file the first time, with a `#` comment that names the change, as [GitHub Docs](https://docs.github.com/en/repositories/working-with-files/using-files/viewing-and-understanding-files) shows. GitHub then hides that commit in the blame view.
53
54
  - Write commit messages in the format that `@next-friday/commitlint-config-friday` defines. Pull request titles use `<type>(<scope>): <subject>`, which `.github/workflows/validate-pr-title.yml` checks.
54
55
  - Keep each pull request focused on one cohesive change.
55
56
  - Describe observable page, route, content, or configuration effects.
@@ -20,6 +20,7 @@
20
20
  "format:check": "prettier --check --cache . && editorconfig-checker",
21
21
  "gates:check": "node scripts/check-agent-gates.ts",
22
22
  "human-gates:check": "node scripts/check-human-gates.ts",
23
+ "intent:check": "node scripts/check-intent.ts",
23
24
  "knip": "knip",
24
25
  "lint": "eslint . --fix --cache --max-warnings=0 --no-inline-config",
25
26
  "lint:check": "eslint . --cache --max-warnings=0 --no-inline-config",
@@ -38,7 +39,7 @@
38
39
  "test:watch": "vitest",
39
40
  "typecheck": "next typegen && tsc --noEmit -p tsconfig.json",
40
41
  "verify": "pnpm verify:static && pnpm test:coverage && pnpm build",
41
- "verify:static": "pnpm suppressions:check && pnpm rules:check && pnpm gates:check && pnpm human-gates:check && pnpm lint:check && pnpm format:check && pnpm sort-package-json:check && pnpm peers:check && pnpm typecheck && pnpm knip && pnpm depcruise"
42
+ "verify:static": "pnpm suppressions:check && pnpm rules:check && pnpm gates:check && pnpm human-gates:check && pnpm intent:check && pnpm lint:check && pnpm format:check && pnpm sort-package-json:check && pnpm peers:check && pnpm typecheck && pnpm knip && pnpm depcruise"
42
43
  },
43
44
  "dependencies": {
44
45
  "next": "16.4.3",
@@ -81,6 +81,8 @@ jobs:
81
81
  run: pnpm gates:check
82
82
  - name: Check human gates
83
83
  run: pnpm human-gates:check
84
+ - name: Self-test the intent checker
85
+ run: pnpm intent:check
84
86
  - name: Check lint
85
87
  run: pnpm lint:check
86
88
  - name: Check formatting