@next-friday/create-app 9.5.1 → 9.7.0

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 (36) hide show
  1. package/README.md +11 -10
  2. package/dist/main.mjs +109 -14
  3. package/package.json +1 -1
  4. package/templates/base/SETUP.md +12 -2
  5. package/templates/base/plugins/sdlc/claude-code/hooks/check-reply-shape.sh +45 -1
  6. package/templates/base/plugins/sdlc/claude-code/hooks/hooks.json +11 -0
  7. package/templates/base/plugins/sdlc/claude-code/skills/sdlc-mode/SKILL.md +1 -1
  8. package/templates/base/plugins/sdlc/claude-code/skills/sdlc-mode/playbooks/maintain.md +1 -1
  9. package/templates/base/plugins/sdlc/claude-code/skills/sdlc-mode/playbooks/review.md +1 -1
  10. package/templates/base/plugins/sdlc/claude-code/skills/setup-sdlc/SKILL.md +2 -0
  11. package/templates/base/plugins/sdlc/cursor/hooks/check-reply-shape.sh +45 -1
  12. package/templates/base/plugins/sdlc/cursor/hooks/hooks.json +5 -0
  13. package/templates/base/plugins/sdlc/cursor/skills/sdlc-mode/SKILL.md +1 -1
  14. package/templates/base/plugins/sdlc/cursor/skills/sdlc-mode/playbooks/maintain.md +1 -1
  15. package/templates/base/plugins/sdlc/cursor/skills/sdlc-mode/playbooks/review.md +1 -1
  16. package/templates/base/plugins/sdlc/cursor/skills/setup-sdlc/SKILL.md +2 -0
  17. package/templates/deploy-railway/turborepo/turbo.json +66 -0
  18. package/templates/nestjs/AGENTS.md +5 -0
  19. package/templates/nestjs/Dockerfile +1 -1
  20. package/templates/nestjs/README.md +16 -2
  21. package/templates/nextjs/AGENTS.md +5 -0
  22. package/templates/nextjs/Dockerfile +1 -1
  23. package/templates/nextjs/README.md +16 -2
  24. package/templates/nodejs/AGENTS.md +5 -0
  25. package/templates/nodejs/Dockerfile +1 -1
  26. package/templates/nodejs/README.md +16 -2
  27. package/templates/payload/AGENTS.md +5 -0
  28. package/templates/payload/Dockerfile +1 -1
  29. package/templates/payload/README.md +16 -2
  30. package/templates/turborepo/AGENTS.md +5 -0
  31. package/templates/turborepo/README.md +16 -2
  32. package/templates/turborepo/apps/app/Dockerfile +1 -1
  33. package/templates/turborepo/apps/backoffice/Dockerfile +1 -1
  34. package/templates/turborepo/turbo.json +1 -7
  35. /package/templates/{base → deploy-railway/base}/railway.json +0 -0
  36. /package/templates/{base → deploy-railway/base}/runbooks/rollback-deploy.md +0 -0
package/README.md CHANGED
@@ -31,7 +31,7 @@ Every stack is built from [`templates/base`](templates/base), which holds the sh
31
31
  - **Code quality:** TypeScript, ESLint and Prettier with the Next Friday configs, Vitest with coverage, fast-check, Knip, dependency-cruiser, editorconfig-checker, and sort-package-json.
32
32
  - **Git hooks:** Husky runs gitleaks, `pnpm rules:check`, and lint-staged before each commit, commitlint on each message, and `pnpm check` before each push. After the first commit, commits to `main` are blocked, and local merges into `main` always are; pushes to `main` are blocked once it exists on the remote.
33
33
  - **CI:** GitHub Actions run the static checks, tests with patch coverage, the build, end-to-end tests for Next.js and Payload, a container build and vulnerability scan, Semgrep, OSV-Scanner, CodeQL, dependency review, OpenSSF Scorecard, gitleaks, TruffleHog, zizmor, and pull request title validation. Dependabot keeps dependencies current.
34
- - **Deployment:** a `Dockerfile` that runs as the `node` user, and a Railway workflow that promotes one commit through `dev`, `uat`, and `prod`.
34
+ - **Deployment:** a `Dockerfile` that runs as the `node` user. With `--deploy railway`, also a Railway workflow that promotes one commit through `dev`, `uat`, and `prod`.
35
35
  - **Agent rules:** `CLAUDE.md` imports `AGENTS.md`. Shared rules live in `.agents/rules`, with hooks that enforce them in Claude Code and Cursor.
36
36
 
37
37
  ## Requirements
@@ -44,17 +44,18 @@ Every stack is built from [`templates/base`](templates/base), which holds the sh
44
44
  ## Options
45
45
 
46
46
  ```text
47
- pnpm create @next-friday/app [name] [--stack nodejs|nestjs|nextjs|payload|turborepo]
47
+ pnpm create @next-friday/app [name] [--stack nodejs|nestjs|nextjs|payload|turborepo] [--deploy none|railway]
48
48
  ```
49
49
 
50
- | Option | Description |
51
- | :--------------- | :---------------------------------------------------------------------------- |
52
- | `name` | Project directory and package name. Asked when left out. |
53
- | `--stack` | `nodejs`, `nestjs`, `nextjs`, `payload`, or `turborepo`. Asked when left out. |
54
- | `--skip-git` | Do not run `git init`. |
55
- | `--skip-install` | Do not run `pnpm install`. |
56
- | `-v, --version` | Print the version. |
57
- | `-h, --help` | Print the usage. |
50
+ | Option | Description |
51
+ | :--------------- | :---------------------------------------------------------------------------------------- |
52
+ | `name` | Project directory and package name. Asked when left out. |
53
+ | `--stack` | `nodejs`, `nestjs`, `nextjs`, `payload`, or `turborepo`. Asked when left out. |
54
+ | `--deploy` | Deploy target: `none` or `railway`. The default is `none`, which adds no deploy workflow. |
55
+ | `--skip-git` | Do not run `git init`. |
56
+ | `--skip-install` | Do not run `pnpm install`. |
57
+ | `-v, --version` | Print the version. |
58
+ | `-h, --help` | Print the usage. |
58
59
 
59
60
  The name must:
60
61
 
package/dist/main.mjs CHANGED
@@ -117,6 +117,16 @@ function validateProjectName(name, cwd) {
117
117
  function ownEntries(directory) {
118
118
  return readdirSync(directory).filter((entry) => entry !== ".git");
119
119
  }
120
+ // src/deploy-targets.ts
121
+ const deployTargets = ["none", "railway"];
122
+ /**
123
+ * Check whether a string names a supported deploy target.
124
+ * @param value - Deploy target from the command line.
125
+ * @returns Whether `value` is a supported deploy target.
126
+ */
127
+ function isDeployTarget(value) {
128
+ return deployTargets.some((target) => target === value);
129
+ }
120
130
  // src/stacks.ts
121
131
  const stacks = [
122
132
  {
@@ -173,10 +183,11 @@ function templateName(stack) {
173
183
  return `next-friday-template-${stack}`;
174
184
  }
175
185
  // src/arguments.ts
176
- const usage = `Usage: pnpm create @next-friday/app [name] [--stack ${stacks.map((stack) => stack.id).join("|")}]
186
+ const usage = `Usage: pnpm create @next-friday/app [name] [--stack ${stacks.map((stack) => stack.id).join("|")}] [--deploy ${deployTargets.join("|")}]
177
187
 
178
188
  name Project directory and package name, or . for the current directory. Asked when left out.
179
189
  --stack Template stack. Asked when left out.
190
+ --deploy Deploy target: ${deployTargets.join(" or ")}. The default is none.
180
191
  --skip-git Do not run git init.
181
192
  --skip-install Do not run pnpm install.
182
193
  -v, --version Show the version.
@@ -192,6 +203,10 @@ function parseCliArguments(cliArguments) {
192
203
  allowPositionals: true,
193
204
  options: {
194
205
  stack: { type: "string" },
206
+ deploy: {
207
+ type: "string",
208
+ default: "none"
209
+ },
195
210
  "skip-git": {
196
211
  type: "boolean",
197
212
  default: false
@@ -212,23 +227,33 @@ function parseCliArguments(cliArguments) {
212
227
  }
213
228
  }
214
229
  });
215
- const { help, stack, version } = values;
230
+ const { deploy, help, stack, version } = values;
216
231
  const isInformational = help || version;
217
232
  if (!isInformational && positionals.length > 1) throw new Error(`Pass one project name, not ${positionals.length}: ${positionals.join(" ")}.`);
218
233
  const [name] = positionals;
219
- if (!isInformational && stack !== void 0 && !isStackId(stack)) {
220
- const ids = stacks.map((supported) => supported.id).join(", ");
221
- throw new Error(`Unknown stack "${stack}". Use one of: ${ids}.`);
222
- }
234
+ if (!isInformational) assertKnownChoices(stack, deploy);
223
235
  return {
224
236
  help,
225
237
  name,
226
238
  version,
239
+ deploy: isDeployTarget(deploy) ? deploy : "none",
227
240
  stack: stack !== void 0 && isStackId(stack) ? stack : void 0,
228
241
  skipGit: values["skip-git"],
229
242
  skipInstall: values["skip-install"]
230
243
  };
231
244
  }
245
+ /**
246
+ * Throw when the stack or the deploy target is not one the CLI supports.
247
+ * @param stack - Stack from the command line, or `undefined` when left out.
248
+ * @param deploy - Deploy target from the command line.
249
+ */
250
+ function assertKnownChoices(stack, deploy) {
251
+ if (stack !== void 0 && !isStackId(stack)) {
252
+ const ids = stacks.map((supported) => supported.id).join(", ");
253
+ throw new Error(`Unknown stack "${stack}". Use one of: ${ids}.`);
254
+ }
255
+ if (!isDeployTarget(deploy)) throw new Error(`Unknown deploy target "${deploy}". Use one of: ${deployTargets.join(", ")}.`);
256
+ }
232
257
  // src/rename.ts
233
258
  /**
234
259
  * Replace the template name with the project name in every text file.
@@ -245,6 +270,45 @@ async function renameProject(directory, from, to) {
245
270
  await writeFile(file, text.replaceAll(from, () => to));
246
271
  }));
247
272
  }
273
+ // src/deploy-sections.ts
274
+ const marker = /^\s*(?:#|\/\/|<!--)\s*deploy:(?<name>[a-z]+)\s*(?:-->\s*)?$/u;
275
+ const endName = "end";
276
+ /**
277
+ * Keep the sections of a template file marked for the chosen deploy target and remove the others.
278
+ * A section starts at a comment line that holds `deploy:<target>` and ends at one that holds `deploy:end`.
279
+ * Every marker line is removed, with a blank line that directly follows it.
280
+ * @param content - Text of a copied template file.
281
+ * @param target - Chosen deploy target.
282
+ * @returns The text for the chosen deploy target.
283
+ */
284
+ function resolveDeploySections(content, target) {
285
+ const kept = [];
286
+ let section;
287
+ let isAfterMarker = false;
288
+ for (const line of content.split("\n")) {
289
+ const name = marker.exec(line)?.groups?.name;
290
+ if (name === void 0) {
291
+ if (!(isAfterMarker && line.trim() === "") && (section ?? target) === target) kept.push(line);
292
+ } else section = sectionAfterMarker(name, section);
293
+ isAfterMarker = name !== void 0;
294
+ }
295
+ if (section !== void 0) throw new Error(`The deploy:${section} section has no deploy:end marker.`);
296
+ return kept.join("\n");
297
+ }
298
+ /**
299
+ * Give the section a marker line leaves open, or throw when the marker is out of place.
300
+ * @param name - Deploy target the marker names, or `end`.
301
+ * @param section - Deploy target of the section the marker sits in, or `undefined` outside a section.
302
+ * @returns The deploy target of the section now open, or `undefined` after an end marker.
303
+ */
304
+ function sectionAfterMarker(name, section) {
305
+ if (name === endName) {
306
+ if (section === void 0) throw new Error("A deploy:end marker has no deploy target section to close.");
307
+ return;
308
+ }
309
+ if (section !== void 0) throw new Error(`A deploy:${name} marker starts inside the deploy:${section} section.`);
310
+ return name;
311
+ }
248
312
  // src/create.ts
249
313
  const templatesDirectory = fileURLToPath(new URL("../templates", import.meta.url));
250
314
  const interruptSignals = [
@@ -276,7 +340,7 @@ async function create(cliArguments) {
276
340
  const target = path.resolve(cwd, name);
277
341
  const directory = existsSync(target) ? realpathSync(target) : target;
278
342
  log.step(`Copying the ${stack} template.`);
279
- await writeProject(stack, directory, manifestName);
343
+ await writeProject(stack, options.deploy, directory, manifestName);
280
344
  log.success(`Created ${manifestName} from the ${stack} template.`);
281
345
  const { git, isInstalled } = setUpProject(directory, options);
282
346
  outro(nextSteps(name, stack, git, isInstalled));
@@ -294,10 +358,11 @@ async function create(cliArguments) {
294
358
  * Copy, rename, and write `.env`, removing what was written when a step fails or Ctrl+C, SIGTERM, or SIGHUP arrives.
295
359
  * An interrupt waits for the running step, so the removal does not race the copy.
296
360
  * @param stack - Supported stack id.
361
+ * @param deploy - Supported deploy target.
297
362
  * @param directory - Project directory.
298
363
  * @param manifestName - Package name for `package.json`.
299
364
  */
300
- async function writeProject(stack, directory, manifestName) {
365
+ async function writeProject(stack, deploy, directory, manifestName) {
301
366
  const kept = existsSync(directory) ? readdirSync(directory) : void 0;
302
367
  let isInterrupted = false;
303
368
  const interrupt = () => {
@@ -305,7 +370,7 @@ async function writeProject(stack, directory, manifestName) {
305
370
  };
306
371
  for (const signal of interruptSignals) process.on(signal, interrupt);
307
372
  try {
308
- await copyTemplate(stack, directory);
373
+ await copyTemplate(stack, deploy, directory);
309
374
  await renameProject(directory, templateName(stack), manifestName);
310
375
  await writeEnvironmentFile(directory);
311
376
  if (isInterrupted) throw new CancelledError();
@@ -382,19 +447,49 @@ function existingGit(directory) {
382
447
  return existsSync(path.join(directory, ".git")) ? "existing" : void 0;
383
448
  }
384
449
  /**
385
- * Copy the shared template files, then the stack's files, into the project directory.
450
+ * Copy the shared template files, the stack's files, and the deploy target's files into the project directory.
386
451
  * npm leaves `.gitignore` out of published packages, so the template keeps it as `_gitignore`.
387
452
  * @param stack - Supported stack id.
453
+ * @param deploy - Supported deploy target.
388
454
  * @param directory - Project directory.
389
455
  */
390
- async function copyTemplate(stack, directory) {
391
- for (const template of ["base", stack]) if (!existsSync(path.join(templatesDirectory, template))) throw new Error(`The ${template} template is missing from ${templatesDirectory}. Reinstall @next-friday/create-app.`);
392
- await cp(path.join(templatesDirectory, "base"), directory, { recursive: true });
393
- await cp(path.join(templatesDirectory, stack), directory, { recursive: true });
456
+ async function copyTemplate(stack, deploy, directory) {
457
+ const deployLayer = deploy === "none" ? void 0 : `deploy-${deploy}`;
458
+ const required = [
459
+ "base",
460
+ stack,
461
+ ...deployLayer === void 0 ? [] : [`${deployLayer}/base`]
462
+ ];
463
+ for (const template of required) if (!existsSync(path.join(templatesDirectory, template))) throw new Error(`The ${template} template is missing from ${templatesDirectory}. Reinstall @next-friday/create-app.`);
464
+ const copyLayer = async (layer) => {
465
+ await cp(path.join(templatesDirectory, layer), directory, { recursive: true });
466
+ };
467
+ await copyLayer("base");
468
+ await copyLayer(stack);
469
+ if (deployLayer !== void 0) {
470
+ await copyLayer(`${deployLayer}/base`);
471
+ if (existsSync(path.join(templatesDirectory, deployLayer, stack))) await copyLayer(`${deployLayer}/${stack}`);
472
+ }
394
473
  await rename(path.join(directory, "_gitignore"), path.join(directory, ".gitignore"));
474
+ await resolveDeployTarget(directory, deploy);
395
475
  await markScriptsExecutable(directory);
396
476
  }
397
477
  /**
478
+ * Resolve the deploy target sections of every copied file that holds a marker.
479
+ * @param directory - Project directory.
480
+ * @param deploy - Supported deploy target.
481
+ */
482
+ async function resolveDeployTarget(directory, deploy) {
483
+ const files = await listProjectFiles(directory);
484
+ await Promise.all(files.map(async (file) => {
485
+ const content = await readFile(file);
486
+ if (!content.includes("deploy:")) return;
487
+ const template = content.toString();
488
+ const resolved = resolveDeploySections(template, deploy);
489
+ if (resolved !== template) await writeFile(file, resolved);
490
+ }));
491
+ }
492
+ /**
398
493
  * Make every file that starts with a shebang executable. `pnpm pack` drops the executable bit,
399
494
  * and the Git and agent hooks run these scripts directly.
400
495
  * @param directory - Project directory.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next-friday/create-app",
3
- "version": "9.5.1",
3
+ "version": "9.7.0",
4
4
  "description": "Create a Next Friday project from a template.",
5
5
  "keywords": [
6
6
  "create",
@@ -43,6 +43,8 @@ In `.github/CODEOWNERS`, add these lines in this order. GitHub uses the last mat
43
43
 
44
44
  Replace every owner above with a GitHub user or team that has write access to the repository. Also replace the owner placeholder in `.github/zizmor.yml`.
45
45
 
46
+ Code owners need: write access to the repository for each owner; for a team, a visible team that itself has write access; and, in a private repository, GitHub Pro, Team, or Enterprise ([code owners](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners)). Until the ruleset of step 3 requires their review, a code owner is asked for a review and a pull request merges without it.
47
+
46
48
  Replace `security@example.com` in `SECURITY.md` with the contact the security policy owner names, such as the security email of the company or of the client.
47
49
 
48
50
  **Check:** on GitHub, open `.github/CODEOWNERS`; GitHub shows no errors in the file. `grep example.com SECURITY.md` prints nothing.
@@ -62,7 +64,10 @@ Replace `security@example.com` in `SECURITY.md` with the contact the security po
62
64
 
63
65
  Rulesets in a private repository need GitHub Pro, Team, or Enterprise; on GitHub Free, make the repository public or upgrade.
64
66
 
65
- - **Secrets and variables → Actions → Variables:** in a private repository, set `CODE_SCANNING` to `true` to run CodeQL, Scorecard, and Dependency Review, which need GitHub Code Security there; public repositories run them without it. Set `DEPLOY_ENABLED` to `true` when each push to `main` that passes CI should deploy to `dev`; the README's deploy section sets up the environments.
67
+ - **Secrets and variables → Actions → Variables:** in a private repository, set `CODE_SCANNING` to `true` to run CodeQL, Scorecard, and Dependency Review, which need GitHub Code Security there; public repositories run them without it. Set `CONTAINER_IMAGE` to `false` to skip the job that builds and scans the container image, for a project that ships no image.
68
+ <!-- deploy:railway -->
69
+ - **Secrets and variables → Actions → Variables:** set `DEPLOY_ENABLED` to `true` when each push to `main` that passes CI should deploy to `dev`; the README's deploy section sets up the environments.
70
+ <!-- deploy:end -->
66
71
  - Create the `needs triage` label, which the issue forms apply: `gh label create "needs triage"`.
67
72
 
68
73
  **Check:** a test pull request shows the two required checks and asks for a code owner's review.
@@ -73,7 +78,7 @@ Replace `security@example.com` in `SECURITY.md` with the contact the security po
73
78
 
74
79
  - **Code Review:** in [**Admin settings → Claude Code**](https://claude.ai/admin-settings/claude-code#code-review), turn on **Allow code review**, install the Claude GitHub App, and add this repository ([set up Code Review](https://code.claude.com/docs/en/code-review-setup)). It needs a Team or Enterprise plan ([availability](https://code.claude.com/docs/en/code-review#availability)). Keep **Review runs** at **After every push**, the default, so every push to a pull request gets a review. Reviews run on Anthropic's infrastructure, so the repository has no review workflow. Anyone with write access can also comment `@claude review` on a pull request to start a review ([comment commands](https://code.claude.com/docs/en/code-review-pull-requests#comment-commands)).
75
80
  - **Managed settings:** deploy the non-negotiable settings and hooks as [managed settings](https://code.claude.com/docs/en/settings), tailored from the example in `.claude/managed-settings.example.json`. Engineers cannot override them. When `strictKnownMarketplaces` is set, as in the example, add the marketplace `next-friday-plugins` of this project to it, or Claude Code does not register the project's plugin.
76
- - **Monitoring:** turn on the [OpenTelemetry export](https://code.claude.com/docs/en/monitoring-usage).
81
+ - **Monitoring:** turn on the [OpenTelemetry export](https://code.claude.com/docs/en/monitoring-usage). It needs an OpenTelemetry collector or backend that the organization runs, and its endpoint. Set `CLAUDE_CODE_ENABLE_TELEMETRY`, the exporters, and `OTEL_EXPORTER_OTLP_ENDPOINT` in managed settings, or each engineer sets them in a shell or in `~/.claude/settings.json`: Claude Code ignores these variables in the repository's `.claude/settings.json`, so the project cannot turn the export on.
77
82
 
78
83
  **Check:** the test pull request gets a **Claude Code Review** check run within a few minutes.
79
84
 
@@ -92,7 +97,12 @@ Replace `security@example.com` in `SECURITY.md` with the contact the security po
92
97
  - **Sources of truth (engineering lead):** for each artifact, name one system as the source of truth: this repository, an existing tool such as Jira, or, as the minimum, a link both ways (the record id in the file and the commit SHA in the record).
93
98
  - **Higher risk (engineering lead):** decide which changes count as higher risk. A tech lead or architect approves their specs and plans.
94
99
  - **Policies (each policy owner, with an engineer):** the plugin ships a default for each area, `plugins/sdlc/claude-code/skills/<area>-policy/SKILL.md` (security: OWASP ASVS 5.0.0 Level 1; UX: WCAG 2.2 Level AA; compliance: the OECD privacy principles; brand: one declared source for every brand value). Review it. To replace a default, add the organization's own rules in `.agents/skills/<area>-policy/SKILL.md` in the project: that file wins over the plugin's default, and the `CODEOWNERS` line of step 2 for that area makes the policy owner its code owner. Do not edit the plugin's copy. Then run `pnpm rules:sync`, which renders the project's own policy skill for Claude Code and Cursor. Test that the skill triggers: ask for the relevant task in different ways and confirm the skill loads each time.
100
+ <!-- deploy:railway -->
95
101
  - **Production gate (engineering leadership, with change management and compliance):** name the release managers and add them as required reviewers on the `prod` environment, as the README's deploy section describes. `.agents/hooks/agent-production-gate.sh` blocks an agent's shell commands that dispatch a `prod` deploy, run a Railway command with `prod` as its environment, or push a release tag. It does not read MCP tools. A release manager whose own agent session must do one of these starts the agent with `RELEASE_APPROVAL` set to the approval record, for example `RELEASE_APPROVAL=CHG-123 claude`. The value lifts the gate only when it matches the extended regular expression on the first line of `.agents/release-approval-form`, shipped as `^[A-Z]+-[0-9]+$`; set that line to the form of your organization's approval records. Each gate hook appends its decisions to `.agents/logs/gate-decisions.jsonl`, which git ignores: the time, the hook, the decision, the session, and the reason for a block, and, when the production gate is lifted, the approval value and the Git user name. A record holds no command text and no path.
102
+ <!-- deploy:end -->
103
+ <!-- deploy:none -->
104
+ - **Production gate (engineering leadership, with change management and compliance):** name the release managers and add them as required reviewers on the `prod` environment, as the README's deploy section describes. `.agents/hooks/agent-production-gate.sh` blocks an agent's shell commands that dispatch a `prod` deploy or push a release tag. It does not read MCP tools. A release manager whose own agent session must do one of these starts the agent with `RELEASE_APPROVAL` set to the approval record, for example `RELEASE_APPROVAL=CHG-123 claude`. The value lifts the gate only when it matches the extended regular expression on the first line of `.agents/release-approval-form`, shipped as `^[A-Z]+-[0-9]+$`; set that line to the form of your organization's approval records. Each gate hook appends its decisions to `.agents/logs/gate-decisions.jsonl`, which git ignores: the time, the hook, the decision, the session, and the reason for a block, and, when the production gate is lifted, the approval value and the Git user name. A record holds no command text and no path.
105
+ <!-- deploy:end -->
96
106
  - **Review (tech lead):** `.agents/review.md` names this stack's generated files to skip; add a path when the project gains a generated file, then run `pnpm rules:sync`, which renders it to `REVIEW.md` for Claude Code Review. Once a month, rate the review findings and tune `.agents/review.md`.
97
107
  - **People without Git experience:** connect GitHub to Claude (claude.ai or Cowork), so Claude commits `intent.md` for them.
98
108
 
@@ -3,8 +3,44 @@ command -v jq > /dev/null || exit 0
3
3
  mode=${1:-}
4
4
  input=$(cat)
5
5
  state_directory=${TMPDIR:-/tmp}/sdlc-reply-shape
6
- conversation=$(printf '%s' "$input" | jq -r '.conversation_id // ""' | tr -cd 'A-Za-z0-9_-')
6
+ conversation=$(printf '%s' "$input" | jq -r '.conversation_id // .session_id // ""')
7
+ conversation=${conversation:+$(printf '%s' "$conversation" | cksum | tr ' ' '-')}
8
+ project=$(printf '%s' "$input" | jq -r '.cwd // .workspace_roots[0] // ""')
9
+ snapshot_file=$state_directory/$conversation.files
10
+
11
+ # A copy of the index and a scratch object store keep the project's own Git data unwritten.
12
+ snapshot() {
13
+ [ -n "$project" ] && [ -n "$conversation" ] || return 1
14
+ repository=$(git -C "$project" rev-parse --show-toplevel 2> /dev/null) || return 1
15
+ index=$(git -C "$repository" rev-parse --git-path index) || return 1
16
+ objects=$(git -C "$repository" rev-parse --git-path objects) || return 1
17
+ case $index in /*) ;; *) index=$repository/$index ;; esac
18
+ case $objects in /*) ;; *) objects=$repository/$objects ;; esac
19
+ scratch=$state_directory/$conversation.git
20
+ rm -rf "$scratch"
21
+ mkdir -p "$scratch/objects" || return 1
22
+ cp "$index" "$scratch/index" 2> /dev/null
23
+ content=$(
24
+ export GIT_INDEX_FILE="$scratch/index" GIT_OBJECT_DIRECTORY="$scratch/objects" GIT_ALTERNATE_OBJECT_DIRECTORIES="$objects"
25
+ git -C "$repository" add -A -- ':(top)' ':(top,exclude)intent' 2> /dev/null
26
+ git -C "$repository" ls-files -s -- ':(top)' ':(top,exclude)intent' | cksum
27
+ )
28
+ rm -rf "$scratch"
29
+ printf '%s|%s' "$repository" "$content"
30
+ }
31
+
32
+ changed_since_snapshot() {
33
+ current=$(snapshot) || return 1
34
+ previous=$(cat "$snapshot_file" 2> /dev/null)
35
+ printf '%s' "$current" > "$snapshot_file"
36
+ [ "${previous%|*}" = "${current%|*}" ] && [ "$previous" != "$current" ]
37
+ }
38
+
7
39
  case $mode in
40
+ snapshot)
41
+ changed_since_snapshot
42
+ exit 0
43
+ ;;
8
44
  save)
9
45
  text=$(printf '%s' "$input" | jq -r '.text // ""')
10
46
  [ -n "$text" ] && [ -n "$conversation" ] || exit 0
@@ -12,12 +48,16 @@ case $mode in
12
48
  exit 0
13
49
  ;;
14
50
  followup)
51
+ changed=false
52
+ changed_since_snapshot && changed=true
15
53
  [ -n "$conversation" ] && [ -f "$state_directory/$conversation" ] || exit 0
16
54
  reply=$(cat "$state_directory/$conversation")
17
55
  rm -f "$state_directory/$conversation"
18
56
  [ "$(printf '%s' "$input" | jq -r '"\(.status // "") \(.loop_count // "")"')" = 'completed 0' ] || exit 0
19
57
  ;;
20
58
  *)
59
+ changed=false
60
+ changed_since_snapshot && changed=true
21
61
  [ "$(printf '%s' "$input" | jq -r '.stop_hook_active // false')" = true ] && exit 0
22
62
  reply=$(printf '%s' "$input" | jq -r '.last_assistant_message // ""')
23
63
  ;;
@@ -48,6 +88,10 @@ case $last in
48
88
  *) problems="${problems}End the reply with a line that starts with **Status:**; nothing may follow it. " ;;
49
89
  esac
50
90
 
91
+ if [ "$changed" = true ] && ! printf '%s\n' "$outside_fences" | grep -q '^\*\*Checks:\*\*'; then
92
+ problems="${problems}A file outside intent/ changed in this turn and the reply has no **Checks:** section. Run the checks the project names and add **Checks:** with each command and the lines of its output that show the result, or with each check that could not run and why. "
93
+ fi
94
+
51
95
  [ -z "$problems" ] && exit 0
52
96
  jq -n --arg mode "$mode" --arg reason "${problems}Send the whole reply again." \
53
97
  'if $mode == "followup" then {followup_message:$reason} else {decision:"block",reason:$reason} end'
@@ -1,5 +1,16 @@
1
1
  {
2
2
  "hooks": {
3
+ "UserPromptSubmit": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "timeout": 10,
8
+ "type": "command",
9
+ "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/check-reply-shape.sh\" snapshot"
10
+ }
11
+ ]
12
+ }
13
+ ],
3
14
  "Stop": [
4
15
  {
5
16
  "hooks": [
@@ -49,7 +49,7 @@ For example, the person writes "who must approve pull request #7 of https://host
49
49
 
50
50
  **Steps:** <only when the session has no task-list tool, and never for the `question` playbook. One line for each numbered step of the playbook: `1. <the step, copied word for word>: done | open | skip: <reason>`. When the turn ran more than one playbook, one list for each, under its file name on a line of its own, such as `playbooks/build.md`.>
51
51
 
52
- **Checks:** <only when a step ran checks, and never for the `question` playbook. Each command, then the lines of its output that show the result, copied from the output.>
52
+ **Checks:** <when a step ran checks, and always when the turn changed a file outside `intent/`, whichever playbook ran. Otherwise never for the `question` playbook. Each command, then the lines of its output that show the result, copied from the output. For a check that could not run, the check and why.>
53
53
 
54
54
  <What the current step produces: the round of questions, or the whole draft. For a question or a request no row fits, the answer.>
55
55
 
@@ -13,6 +13,6 @@ This playbook starts from a finding such as an alert, a monitoring ticket, a fai
13
13
  - Start a subagent that has not seen the diagnosis, give it only the finding, the logs and files you read, and `intent.md`, and have it try to disprove the **Problem** section from the evidence. When you cannot start a subagent, read the evidence again from the first line as if someone else wrote the intent.
14
14
  - When a claim does not survive, move it under **Open questions**, and say in the pull request that the confidence check failed and on which claim.
15
15
  5. Commit `intent.md` on a new branch, push it, and open a pull request ready for review, as the opening paragraph of `playbooks/open-pr.md` says for branches and pull requests. When you cannot open a pull request from this session, say so and give the pushed branch's name.
16
- 6. **Gate: the service owner triages the finding.** The service owner merges the pull request to fix the finding now, leaves it open to schedule it, or closes it to dismiss it. A dismissal tunes the bands: the service owner adjusts `bands.yaml` so the same noise raises fewer findings. The service owner asks the product owner to decide a product-facing finding. You do none of these. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
16
+ 6. **Gate: the service owner triages the finding.** The service owner merges the pull request to fix the finding now, comments on it with when the finding is scheduled and leaves it open, or closes it to dismiss it. An open pull request with no such comment is undecided: say so, and never report it as scheduled. A dismissal tunes the bands: the service owner adjusts `bands.yaml` so the same noise raises fewer findings. The service owner asks the product owner to decide a product-facing finding. You do none of these. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
17
17
 
18
18
  **Next:** `playbooks/design.md`; an agent; when the service owner has merged the intent's pull request and a person or a job sends `/sdlc:sdlc-mode` with the intent's path.
@@ -1,6 +1,6 @@
1
1
  # Review phase: review the pull request and drive it to green
2
2
 
3
- 1. Find what to review: the pull request this session just opened, or the pull request or branch the request names. When there is no pull request, or this session cannot reach its host, review the branch's diff against the default branch and put the findings in your reply in place of a comment.
3
+ 1. Find what to review: the pull request this session just opened, or the pull request or branch the request names. When there is no pull request, or this session cannot reach its host, review the branch's diff against the default branch and put the findings in your reply in place of a comment. When a pull request exists and this session cannot post on it, the findings are not in the pull request's history: the **Status** line says that the findings are not on the pull request and that a person posts them there, step 3 stays `open`, and you do not report the review as done.
4
4
  2. Review with a fresh context. Start a subagent that has not seen the work. Give it only the diff, `REVIEW.md`, the change's `intent/<change>/spec.md` and `plan.md`, and the four policy files in force, found as `playbooks/design.md` step 2 says. When you cannot start a subagent, read the diff again from the first line as if someone else wrote it. The review runs each pass in `REVIEW.md` and reports each finding with its pass, **Important** or **Nit**, `path:line`, and the evidence. A change that is not a fix, an intent, or a spec, and has no `spec.md` or no `plan.md`, is itself a finding.
5
5
  3. Post the findings as one comment on the pull request, most severe first. Write "No findings" when there are none. A finding does not decide the merge. When a finding flags a mistake that an earlier review of this project flagged too, its correction goes into the project's agent instructions as part of this review: in the push of step 4, or, on a pull request or a branch this session did not create, as the exact line in this comment.
6
6
  4. On a pull request or a branch that this session did not create, change nothing and push nothing. Skip the rest of this step, and in step 5 only read and answer. Otherwise, fix every Important finding. Run the checks the project's agent instructions name, and push. Answer each finding you do not fix with the reason.
@@ -46,6 +46,8 @@ Do these steps in order. Run again with the same answers, the skill writes the s
46
46
  - `2. Owners: not done. .github/CODEOWNERS still holds the placeholder.`
47
47
  - `3. Repository settings: a person checks this. They live on the host, not in the repository.`
48
48
 
49
+ Report code owners, the ruleset of the default branch, and the OpenTelemetry export each on a line of its own, with what `SETUP.md` says the step needs, such as `Ruleset of main: a person checks this. It needs a repository admin, and in a private repository GitHub Pro, Team, or Enterprise.` A step that is not done fails no check.
50
+
49
51
  7. **Do the `not done` steps an agent can do:** a step whose whole work is editing a file of the repository with values the repository or the request already gives. Do not redo a `done` step. Do not edit a file the project's rules or agent settings reserve for a person, such as rules, hooks, lint, format, and commit configuration, and the agent settings themselves; list it with the exact new text and the file it goes in. Commit nothing. After each step, run its **Check** again and report the result.
50
52
  8. **List the steps a person does outside the repository,** each with who does it, from `SETUP.md`, and the full URL of the page where it is done when `SETUP.md` or the repository names one.
51
53
  9. **Write the reply** by the reply rules in `../sdlc-mode/SKILL.md`. After them, in this order: the budget line and the four role lines as written; each step of `SETUP.md` with its state; what you did; what a person does.
@@ -3,8 +3,44 @@ command -v jq > /dev/null || exit 0
3
3
  mode=${1:-}
4
4
  input=$(cat)
5
5
  state_directory=${TMPDIR:-/tmp}/sdlc-reply-shape
6
- conversation=$(printf '%s' "$input" | jq -r '.conversation_id // ""' | tr -cd 'A-Za-z0-9_-')
6
+ conversation=$(printf '%s' "$input" | jq -r '.conversation_id // .session_id // ""')
7
+ conversation=${conversation:+$(printf '%s' "$conversation" | cksum | tr ' ' '-')}
8
+ project=$(printf '%s' "$input" | jq -r '.cwd // .workspace_roots[0] // ""')
9
+ snapshot_file=$state_directory/$conversation.files
10
+
11
+ # A copy of the index and a scratch object store keep the project's own Git data unwritten.
12
+ snapshot() {
13
+ [ -n "$project" ] && [ -n "$conversation" ] || return 1
14
+ repository=$(git -C "$project" rev-parse --show-toplevel 2> /dev/null) || return 1
15
+ index=$(git -C "$repository" rev-parse --git-path index) || return 1
16
+ objects=$(git -C "$repository" rev-parse --git-path objects) || return 1
17
+ case $index in /*) ;; *) index=$repository/$index ;; esac
18
+ case $objects in /*) ;; *) objects=$repository/$objects ;; esac
19
+ scratch=$state_directory/$conversation.git
20
+ rm -rf "$scratch"
21
+ mkdir -p "$scratch/objects" || return 1
22
+ cp "$index" "$scratch/index" 2> /dev/null
23
+ content=$(
24
+ export GIT_INDEX_FILE="$scratch/index" GIT_OBJECT_DIRECTORY="$scratch/objects" GIT_ALTERNATE_OBJECT_DIRECTORIES="$objects"
25
+ git -C "$repository" add -A -- ':(top)' ':(top,exclude)intent' 2> /dev/null
26
+ git -C "$repository" ls-files -s -- ':(top)' ':(top,exclude)intent' | cksum
27
+ )
28
+ rm -rf "$scratch"
29
+ printf '%s|%s' "$repository" "$content"
30
+ }
31
+
32
+ changed_since_snapshot() {
33
+ current=$(snapshot) || return 1
34
+ previous=$(cat "$snapshot_file" 2> /dev/null)
35
+ printf '%s' "$current" > "$snapshot_file"
36
+ [ "${previous%|*}" = "${current%|*}" ] && [ "$previous" != "$current" ]
37
+ }
38
+
7
39
  case $mode in
40
+ snapshot)
41
+ changed_since_snapshot
42
+ exit 0
43
+ ;;
8
44
  save)
9
45
  text=$(printf '%s' "$input" | jq -r '.text // ""')
10
46
  [ -n "$text" ] && [ -n "$conversation" ] || exit 0
@@ -12,12 +48,16 @@ case $mode in
12
48
  exit 0
13
49
  ;;
14
50
  followup)
51
+ changed=false
52
+ changed_since_snapshot && changed=true
15
53
  [ -n "$conversation" ] && [ -f "$state_directory/$conversation" ] || exit 0
16
54
  reply=$(cat "$state_directory/$conversation")
17
55
  rm -f "$state_directory/$conversation"
18
56
  [ "$(printf '%s' "$input" | jq -r '"\(.status // "") \(.loop_count // "")"')" = 'completed 0' ] || exit 0
19
57
  ;;
20
58
  *)
59
+ changed=false
60
+ changed_since_snapshot && changed=true
21
61
  [ "$(printf '%s' "$input" | jq -r '.stop_hook_active // false')" = true ] && exit 0
22
62
  reply=$(printf '%s' "$input" | jq -r '.last_assistant_message // ""')
23
63
  ;;
@@ -48,6 +88,10 @@ case $last in
48
88
  *) problems="${problems}End the reply with a line that starts with **Status:**; nothing may follow it. " ;;
49
89
  esac
50
90
 
91
+ if [ "$changed" = true ] && ! printf '%s\n' "$outside_fences" | grep -q '^\*\*Checks:\*\*'; then
92
+ problems="${problems}A file outside intent/ changed in this turn and the reply has no **Checks:** section. Run the checks the project names and add **Checks:** with each command and the lines of its output that show the result, or with each check that could not run and why. "
93
+ fi
94
+
51
95
  [ -z "$problems" ] && exit 0
52
96
  jq -n --arg mode "$mode" --arg reason "${problems}Send the whole reply again." \
53
97
  'if $mode == "followup" then {followup_message:$reason} else {decision:"block",reason:$reason} end'
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "version": 1,
3
3
  "hooks": {
4
+ "beforeSubmitPrompt": [
5
+ {
6
+ "command": "sh \"${CURSOR_PLUGIN_ROOT}/hooks/check-reply-shape.sh\" snapshot"
7
+ }
8
+ ],
4
9
  "afterAgentResponse": [
5
10
  {
6
11
  "command": "sh \"${CURSOR_PLUGIN_ROOT}/hooks/check-reply-shape.sh\" save"
@@ -49,7 +49,7 @@ For example, the person writes "who must approve pull request #7 of https://host
49
49
 
50
50
  **Steps:** <only when the session has no task-list tool, and never for the `question` playbook. One line for each numbered step of the playbook: `1. <the step, copied word for word>: done | open | skip: <reason>`. When the turn ran more than one playbook, one list for each, under its file name on a line of its own, such as `playbooks/build.md`.>
51
51
 
52
- **Checks:** <only when a step ran checks, and never for the `question` playbook. Each command, then the lines of its output that show the result, copied from the output.>
52
+ **Checks:** <when a step ran checks, and always when the turn changed a file outside `intent/`, whichever playbook ran. Otherwise never for the `question` playbook. Each command, then the lines of its output that show the result, copied from the output. For a check that could not run, the check and why.>
53
53
 
54
54
  <What the current step produces: the round of questions, or the whole draft. For a question or a request no row fits, the answer.>
55
55
 
@@ -13,6 +13,6 @@ This playbook starts from a finding such as an alert, a monitoring ticket, a fai
13
13
  - Start a subagent that has not seen the diagnosis, give it only the finding, the logs and files you read, and `intent.md`, and have it try to disprove the **Problem** section from the evidence. When you cannot start a subagent, read the evidence again from the first line as if someone else wrote the intent.
14
14
  - When a claim does not survive, move it under **Open questions**, and say in the pull request that the confidence check failed and on which claim.
15
15
  5. Commit `intent.md` on a new branch, push it, and open a pull request ready for review, as the opening paragraph of `playbooks/open-pr.md` says for branches and pull requests. When you cannot open a pull request from this session, say so and give the pushed branch's name.
16
- 6. **Gate: the service owner triages the finding.** The service owner merges the pull request to fix the finding now, leaves it open to schedule it, or closes it to dismiss it. A dismissal tunes the bands: the service owner adjusts `bands.yaml` so the same noise raises fewer findings. The service owner asks the product owner to decide a product-facing finding. You do none of these. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
16
+ 6. **Gate: the service owner triages the finding.** The service owner merges the pull request to fix the finding now, comments on it with when the finding is scheduled and leaves it open, or closes it to dismiss it. An open pull request with no such comment is undecided: say so, and never report it as scheduled. A dismissal tunes the bands: the service owner adjusts `bands.yaml` so the same noise raises fewer findings. The service owner asks the product owner to decide a product-facing finding. You do none of these. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
17
17
 
18
18
  **Next:** `playbooks/design.md`; an agent; when the service owner has merged the intent's pull request and a person or a job sends `/sdlc:sdlc-mode` with the intent's path.
@@ -1,6 +1,6 @@
1
1
  # Review phase: review the pull request and drive it to green
2
2
 
3
- 1. Find what to review: the pull request this session just opened, or the pull request or branch the request names. When there is no pull request, or this session cannot reach its host, review the branch's diff against the default branch and put the findings in your reply in place of a comment.
3
+ 1. Find what to review: the pull request this session just opened, or the pull request or branch the request names. When there is no pull request, or this session cannot reach its host, review the branch's diff against the default branch and put the findings in your reply in place of a comment. When a pull request exists and this session cannot post on it, the findings are not in the pull request's history: the **Status** line says that the findings are not on the pull request and that a person posts them there, step 3 stays `open`, and you do not report the review as done.
4
4
  2. Review with a fresh context. Start a subagent that has not seen the work. Give it only the diff, `REVIEW.md`, the change's `intent/<change>/spec.md` and `plan.md`, and the four policy files in force, found as `playbooks/design.md` step 2 says. When you cannot start a subagent, read the diff again from the first line as if someone else wrote it. The review runs each pass in `REVIEW.md` and reports each finding with its pass, **Important** or **Nit**, `path:line`, and the evidence. A change that is not a fix, an intent, or a spec, and has no `spec.md` or no `plan.md`, is itself a finding.
5
5
  3. Post the findings as one comment on the pull request, most severe first. Write "No findings" when there are none. A finding does not decide the merge. When a finding flags a mistake that an earlier review of this project flagged too, its correction goes into the project's agent instructions as part of this review: in the push of step 4, or, on a pull request or a branch this session did not create, as the exact line in this comment.
6
6
  4. On a pull request or a branch that this session did not create, change nothing and push nothing. Skip the rest of this step, and in step 5 only read and answer. Otherwise, fix every Important finding. Run the checks the project's agent instructions name, and push. Answer each finding you do not fix with the reason.
@@ -46,6 +46,8 @@ Do these steps in order. Run again with the same answers, the skill writes the s
46
46
  - `2. Owners: not done. .github/CODEOWNERS still holds the placeholder.`
47
47
  - `3. Repository settings: a person checks this. They live on the host, not in the repository.`
48
48
 
49
+ Report code owners, the ruleset of the default branch, and the OpenTelemetry export each on a line of its own, with what `SETUP.md` says the step needs, such as `Ruleset of main: a person checks this. It needs a repository admin, and in a private repository GitHub Pro, Team, or Enterprise.` A step that is not done fails no check.
50
+
49
51
  7. **Do the `not done` steps an agent can do:** a step whose whole work is editing a file of the repository with values the repository or the request already gives. Do not redo a `done` step. Do not edit a file the project's rules or agent settings reserve for a person, such as rules, hooks, lint, format, and commit configuration, and the agent settings themselves; list it with the exact new text and the file it goes in. Commit nothing. After each step, run its **Check** again and report the result.
50
52
  8. **List the steps a person does outside the repository,** each with who does it, from `SETUP.md`, and the full URL of the page where it is done when `SETUP.md` or the repository names one.
51
53
  9. **Write the reply** by the reply rules in `../sdlc-mode/SKILL.md`. After them, in this order: the budget line and the four role lines as written; each step of `SETUP.md` with its state; what you did; what a person does.
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "https://turborepo.dev/schema.json",
3
+ "futureFlags": {
4
+ "affectedUsingTaskInputs": true
5
+ },
6
+ "globalDependencies": [
7
+ ".dockerignore",
8
+ ".env",
9
+ "package.json",
10
+ "pnpm-workspace.yaml",
11
+ "railway.json"
12
+ ],
13
+ "globalPassThroughEnv": [
14
+ "APP_URL",
15
+ "CI",
16
+ "DATABASE_URL",
17
+ "NEXT_TELEMETRY_DISABLED",
18
+ "PAYLOAD_SECRET"
19
+ ],
20
+ "tasks": {
21
+ "build": {
22
+ "dependsOn": ["^build"],
23
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"],
24
+ "outputs": [".next/**", "!.next/cache/**", "!.next/dev/**"]
25
+ },
26
+ "depcruise": {
27
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
28
+ },
29
+ "dev": {
30
+ "cache": false,
31
+ "persistent": true,
32
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
33
+ },
34
+ "start": {
35
+ "cache": false,
36
+ "persistent": true,
37
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
38
+ },
39
+ "test": {
40
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
41
+ },
42
+ "test:changed": {
43
+ "cache": false,
44
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
45
+ },
46
+ "test:coverage": {
47
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"],
48
+ "outputs": ["coverage/**"]
49
+ },
50
+ "test:e2e": {
51
+ "cache": false,
52
+ "dependsOn": ["build"],
53
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
54
+ },
55
+ "test:watch": {
56
+ "cache": false,
57
+ "persistent": true,
58
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"]
59
+ },
60
+ "typecheck": {
61
+ "dependsOn": ["^typecheck"],
62
+ "inputs": ["$TURBO_DEFAULT$", "!README.md"],
63
+ "outputs": [".next/types/**", "next-env.d.ts"]
64
+ }
65
+ }
66
+ }
@@ -45,7 +45,12 @@ The `verifier` subagent starts the app with `pnpm start:dev`. It exercises the c
45
45
 
46
46
  When Claude makes a mistake twice, add the correction here.
47
47
 
48
+ <!-- deploy:railway -->
48
49
  - **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. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. 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
+ <!-- deploy:end -->
51
+ <!-- deploy:none -->
52
+ - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. 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 passes it.
53
+ <!-- deploy:end -->
49
54
  - **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
50
55
  - **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
51
56
  - **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
@@ -1,4 +1,4 @@
1
- # Runs on any OCI platform (Railway, Fly.io, Cloud Run). The platform sets `PORT`.
1
+ # Runs on any OCI container platform. The platform sets `PORT`.
2
2
  FROM \
3
3
  node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
4
4
  AS base
@@ -34,13 +34,15 @@ CI also runs the build, the end-to-end tests, the container image scan, and the
34
34
 
35
35
  ## Container
36
36
 
37
- The `Dockerfile` builds a production image that runs as a non-root user (UID 1000). The platform must set `PORT`; Railway sets it automatically. The build needs `pnpm-lock.yaml`, so run `pnpm install` once before the first `docker build`.
37
+ The `Dockerfile` builds a production image that runs as a non-root user (UID 1000). The platform must set `PORT`. The build needs `pnpm-lock.yaml`, so run `pnpm install` once before the first `docker build`.
38
38
 
39
39
  ```sh
40
40
  docker build -t app .
41
41
  docker run --env PORT=3000 --publish 3000:3000 app
42
42
  ```
43
43
 
44
+ <!-- deploy:railway -->
45
+
44
46
  ## Deploy
45
47
 
46
48
  `.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
@@ -56,6 +58,18 @@ docker run --env PORT=3000 --publish 3000:3000 app
56
58
  3. Set the repository variable `DEPLOY_ENABLED` to `true`. Each push to `main` that passes CI then deploys to `dev`.
57
59
  4. Promote with **Actions → Deploy → Run workflow**, giving the environment and the full commit SHA. The workflow requires a successful CI run on `main` for that commit and a successful deployment to the previous environment, then waits for `/health`.
58
60
 
59
- Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
61
+ <!-- deploy:end -->
62
+
63
+ <!-- deploy:none -->
64
+
65
+ ## Deploy
66
+
67
+ The project has no deploy step yet. A deploy step needs:
68
+
69
+ - a workflow under `.github/workflows/` that deploys;
70
+ - the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
71
+ - the image the `Dockerfile` builds.
72
+
73
+ <!-- deploy:end -->
60
74
 
61
75
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
@@ -46,7 +46,12 @@ The `verifier` subagent starts the app with `pnpm dev`. It exercises the changed
46
46
  When Claude makes a mistake twice, add the correction here.
47
47
 
48
48
  - **First end-to-end run**: run `pnpm exec playwright install chromium` once. `pnpm test:e2e` starts the development server unless one is already running.
49
+ <!-- deploy:railway -->
49
50
  - **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. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. 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.
51
+ <!-- deploy:end -->
52
+ <!-- deploy:none -->
53
+ - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. 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 passes it.
54
+ <!-- deploy:end -->
50
55
  - **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
51
56
  - **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
52
57
  - **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
@@ -1,4 +1,4 @@
1
- # Runs on any OCI platform (Railway, Fly.io, Cloud Run). On Vercel, deploy without it. The platform sets `PORT`.
1
+ # Runs on any OCI container platform. A platform that builds Next.js itself deploys without it. The platform sets `PORT`.
2
2
  FROM \
3
3
  node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
4
4
  AS build
@@ -32,7 +32,7 @@ CI also runs the build, the end-to-end tests, the container image scan, and the
32
32
 
33
33
  ## Container
34
34
 
35
- The `Dockerfile` builds a standalone production image that runs as the `node` user, for platforms such as Railway. The server listens on `PORT`, or 3000 when it is unset.
35
+ The `Dockerfile` builds a standalone production image that runs as the `node` user for a container platform. The server listens on `PORT`, or 3000 when it is unset.
36
36
 
37
37
  ```sh
38
38
  docker build -t app .
@@ -41,6 +41,8 @@ docker run --publish 3000:3000 app
41
41
 
42
42
  On Vercel, deploy without the image; the Next.js preset takes precedence over the `Dockerfile`.
43
43
 
44
+ <!-- deploy:railway -->
45
+
44
46
  ## Deploy
45
47
 
46
48
  `.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
@@ -56,6 +58,18 @@ On Vercel, deploy without the image; the Next.js preset takes precedence over th
56
58
  3. Set the repository variable `DEPLOY_ENABLED` to `true`. Each push to `main` that passes CI then deploys to `dev`.
57
59
  4. Promote with **Actions → Deploy → Run workflow**, giving the environment and the full commit SHA. The workflow requires a successful CI run on `main` for that commit and a successful deployment to the previous environment, then waits for `/health`.
58
60
 
59
- Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
61
+ <!-- deploy:end -->
62
+
63
+ <!-- deploy:none -->
64
+
65
+ ## Deploy
66
+
67
+ The project has no deploy step yet. A deploy step needs:
68
+
69
+ - a workflow under `.github/workflows/` that deploys;
70
+ - the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
71
+ - the image the `Dockerfile` builds.
72
+
73
+ <!-- deploy:end -->
60
74
 
61
75
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
@@ -44,7 +44,12 @@ The `verifier` subagent starts the app with `pnpm start:dev`. It exercises the c
44
44
 
45
45
  When Claude makes a mistake twice, add the correction here.
46
46
 
47
+ <!-- deploy:railway -->
47
48
  - **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. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. 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.
49
+ <!-- deploy:end -->
50
+ <!-- deploy:none -->
51
+ - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. 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 passes it.
52
+ <!-- deploy:end -->
48
53
  - **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
49
54
  - **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
50
55
  - **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
@@ -1,4 +1,4 @@
1
- # Runs on any OCI platform (Railway, Fly.io, Cloud Run). The platform sets `PORT`.
1
+ # Runs on any OCI container platform. The platform sets `PORT`.
2
2
  FROM \
3
3
  node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
4
4
  AS base
@@ -32,13 +32,15 @@ CI also runs the build, the container image scan, and the security scans, includ
32
32
 
33
33
  ## Container
34
34
 
35
- The `Dockerfile` builds a production image that runs as a non-root user (UID 1000). The platform must set `PORT`; Railway sets it automatically. The build needs `pnpm-lock.yaml`, so run `pnpm install` once before the first `docker build`.
35
+ The `Dockerfile` builds a production image that runs as a non-root user (UID 1000). The platform must set `PORT`. The build needs `pnpm-lock.yaml`, so run `pnpm install` once before the first `docker build`.
36
36
 
37
37
  ```sh
38
38
  docker build -t app .
39
39
  docker run --env PORT=3000 --publish 3000:3000 app
40
40
  ```
41
41
 
42
+ <!-- deploy:railway -->
43
+
42
44
  ## Deploy
43
45
 
44
46
  `.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
@@ -54,6 +56,18 @@ docker run --env PORT=3000 --publish 3000:3000 app
54
56
  3. Set the repository variable `DEPLOY_ENABLED` to `true`. Each push to `main` that passes CI then deploys to `dev`.
55
57
  4. Promote with **Actions → Deploy → Run workflow**, giving the environment and the full commit SHA. The workflow requires a successful CI run on `main` for that commit and a successful deployment to the previous environment, then waits for `/health`.
56
58
 
57
- Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
59
+ <!-- deploy:end -->
60
+
61
+ <!-- deploy:none -->
62
+
63
+ ## Deploy
64
+
65
+ The project has no deploy step yet. A deploy step needs:
66
+
67
+ - a workflow under `.github/workflows/` that deploys;
68
+ - the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
69
+ - the image the `Dockerfile` builds.
70
+
71
+ <!-- deploy:end -->
58
72
 
59
73
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
@@ -49,7 +49,12 @@ The `verifier` subagent starts the app with `pnpm dev`, which needs a PostgreSQL
49
49
  When Claude makes a mistake twice, add the correction here.
50
50
 
51
51
  - **Database for `pnpm dev` and end-to-end tests**: both need a PostgreSQL database in `DATABASE_URL` and `PAYLOAD_SECRET` in `.env`; the README lists every variable. Run `pnpm exec playwright install chromium` once before the first `pnpm test:e2e`.
52
+ <!-- deploy:railway -->
52
53
  - **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. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. 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.
54
+ <!-- deploy:end -->
55
+ <!-- deploy:none -->
56
+ - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. 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 passes it.
57
+ <!-- deploy:end -->
53
58
  - **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
54
59
  - **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
55
60
  - **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
@@ -1,4 +1,4 @@
1
- # Runs on any OCI platform (Railway, Fly.io, Cloud Run). On Vercel, deploy without it. The platform sets `PORT`.
1
+ # Runs on any OCI container platform. A platform that builds Next.js itself deploys without it. The platform sets `PORT`.
2
2
  FROM \
3
3
  node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
4
4
  AS build
@@ -48,7 +48,7 @@ CI also runs the build, the end-to-end tests, the container image scan, and the
48
48
 
49
49
  ## Container
50
50
 
51
- The `Dockerfile` builds a standalone production image that runs as the `node` user, for platforms such as Railway. The server listens on `PORT`, or 3000 when it is unset, and needs `DATABASE_URL`, `PAYLOAD_SECRET`, and `APP_URL`, the public URL that Payload accepts cookie-authenticated and cross-origin requests from.
51
+ The `Dockerfile` builds a standalone production image that runs as the `node` user for a container platform. The server listens on `PORT`, or 3000 when it is unset, and needs `DATABASE_URL`, `PAYLOAD_SECRET`, and `APP_URL`, the public URL that Payload accepts cookie-authenticated and cross-origin requests from.
52
52
 
53
53
  ```sh
54
54
  docker build -t app .
@@ -61,6 +61,8 @@ The config sets no `email` adapter, so Payload writes every email, including pas
61
61
 
62
62
  On Vercel, deploy without the image; the Next.js preset takes precedence over the `Dockerfile`.
63
63
 
64
+ <!-- deploy:railway -->
65
+
64
66
  ## Deploy
65
67
 
66
68
  `.github/workflows/deploy.yml` deploys the `Dockerfile` to Railway and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. The workflow stays off until you configure it:
@@ -82,6 +84,18 @@ On Vercel, deploy without the image; the Next.js preset takes precedence over th
82
84
  3. Set the repository variable `DEPLOY_ENABLED` to `true`. Each push to `main` that passes CI then deploys to `dev`.
83
85
  4. Promote with **Actions → Deploy → Run workflow**, giving the environment and the full commit SHA. The workflow requires a successful CI run on `main` for that commit and a successful deployment to the previous environment, then waits for `/health`.
84
86
 
85
- Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
87
+ <!-- deploy:end -->
88
+
89
+ <!-- deploy:none -->
90
+
91
+ ## Deploy
92
+
93
+ The project has no deploy step yet. A deploy step needs:
94
+
95
+ - a workflow under `.github/workflows/` that deploys;
96
+ - the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
97
+ - the image the `Dockerfile` builds.
98
+
99
+ <!-- deploy:end -->
86
100
 
87
101
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
@@ -52,7 +52,12 @@ When Claude makes a mistake twice, add the correction here.
52
52
 
53
53
  - **Next.js docs live per app**: this Next.js version differs from your training data. Read the guide in `apps/app/node_modules/next/dist/docs/` or `apps/backoffice/node_modules/next/dist/docs/` before writing Next.js code; the workspace root has no `next` package.
54
54
  - **Database for `pnpm dev` and end-to-end tests**: the backoffice needs a PostgreSQL database in `DATABASE_URL` and `PAYLOAD_SECRET` in `.env`; the README lists every variable. Run `pnpm --filter ./apps/app exec playwright install chromium` once before the first `pnpm test:e2e`.
55
+ <!-- deploy:railway -->
55
56
  - **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. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. 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.
57
+ <!-- deploy:end -->
58
+ <!-- deploy:none -->
59
+ - **Production gate**: `.agents/hooks/agent-production-gate.sh` blocks a shell command that deploys to `prod` or pushes a release tag, unless a release manager started the session with `RELEASE_APPROVAL` set. Any value lifts the gate except an empty value, `0`, `false`, and `no`, in any letter case. 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 passes it.
60
+ <!-- deploy:end -->
56
61
  - **Issues**: open an issue only to hand work to another person, never to give a pull request a number to close. A pull request closes an issue that already exists with `Closes #<issue>`.
57
62
  - **What belongs in the repository**: files, commit messages, and pull requests hold the change, its reason, and the checks. Write no date, log, history, mistake count, token figure, or session note into a file.
58
63
  - **Sourced requirements**: add a requirement to an intent, spec, or plan only when a person decided it or a named source says it. This includes a requirement a review bot suggests. When no person decided it and no source says it, leave it out and answer the thread with that.
@@ -61,6 +61,8 @@ Uploaded images, up to 10 MB each, are written to the backoffice container files
61
61
 
62
62
  On Vercel, deploy an app without its image. The Next.js preset takes precedence over the Dockerfile.
63
63
 
64
+ <!-- deploy:railway -->
65
+
64
66
  ## Deploy
65
67
 
66
68
  `.github/workflows/deploy.yml` deploys both apps and promotes one commit through `dev`, `uat`, and `prod`. `railway.json` sets the Railway health check to `/health`. Create one Railway service per app, with that service's Dockerfile path set to `apps/app/Dockerfile` or `apps/backoffice/Dockerfile`. The workflow stays off until you configure it:
@@ -84,7 +86,19 @@ On Vercel, deploy an app without its image. The Next.js preset takes precedence
84
86
  3. Set the repository variable `DEPLOY_ENABLED` to `true`. Each push to `main` that passes CI then deploys to `dev`. The workflow compares the commit with the commit of the last successful deploy to that environment, found from the successful `deploy.yml` and `ci.yml` runs on `main`, and runs `railway up` only for services whose packages changed. A promotion through `uat` or `prod` uses the same comparison against the commit last deployed there, so several commits still ship every changed service. The first deploy to an environment ships both services.
85
87
  4. Promote with **Actions → Deploy → Run workflow**, giving the environment and the full commit SHA. The workflow requires a successful CI run on `main` for that commit and a successful deployment to the previous environment, then waits for `/health` on each service it uploaded.
86
88
 
87
- Remove the workflow, `railway.json`, and the `deploy-dev` job in `ci.yml` if the project deploys elsewhere.
89
+ <!-- deploy:end -->
90
+
91
+ <!-- deploy:none -->
92
+
93
+ ## Deploy
94
+
95
+ The project has no deploy step yet. A deploy step needs:
96
+
97
+ - a workflow under `.github/workflows/` that deploys;
98
+ - the `prod` environment under **Settings → Environments**, with the release managers as its required reviewers;
99
+ - the image the `Dockerfile` builds.
100
+
101
+ <!-- deploy:end -->
88
102
 
89
103
  ## Mobile app
90
104
 
@@ -94,6 +108,6 @@ To add an Expo app, create it under `apps/mobile`:
94
108
  2. Use `@expo/fingerprint` with `expo-github-action/fingerprint` so a pull request requests a native build only when the native layer changed ([Expo fingerprint](https://docs.expo.dev/versions/latest/sdk/fingerprint/)).
95
109
  3. Keep a single `react-native` version in the workspace. Duplicate React Native versions in one monorepo are not supported ([Expo: Monorepos](https://docs.expo.dev/guides/monorepos/)).
96
110
 
97
- EAS Build and Submit replace Railway for that app. CI `--affected` already includes `apps/*` and `packages/*`, so an Expo app is skipped when it did not change.
111
+ EAS Build and Submit deploy that app, in place of the project's deploy step. CI `--affected` already includes `apps/*` and `packages/*`, so an Expo app is skipped when it did not change.
98
112
 
99
113
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.
@@ -1,4 +1,4 @@
1
- # Runs on any OCI platform (Railway, Fly.io, Cloud Run). On Vercel, deploy without it. The platform sets `PORT`.
1
+ # Runs on any OCI container platform. A platform that builds Next.js itself deploys without it. The platform sets `PORT`.
2
2
  FROM \
3
3
  node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
4
4
  AS build
@@ -1,4 +1,4 @@
1
- # Runs on any OCI platform (Railway, Fly.io, Cloud Run). On Vercel, deploy without it. The platform sets `PORT`.
1
+ # Runs on any OCI container platform. A platform that builds Next.js itself deploys without it. The platform sets `PORT`.
2
2
  FROM \
3
3
  node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 \
4
4
  AS build
@@ -3,13 +3,7 @@
3
3
  "futureFlags": {
4
4
  "affectedUsingTaskInputs": true
5
5
  },
6
- "globalDependencies": [
7
- ".dockerignore",
8
- ".env",
9
- "package.json",
10
- "pnpm-workspace.yaml",
11
- "railway.json"
12
- ],
6
+ "globalDependencies": [".dockerignore", ".env", "package.json", "pnpm-workspace.yaml"],
13
7
  "globalPassThroughEnv": [
14
8
  "APP_URL",
15
9
  "CI",