@skyf0xx/hedgehog 0.1.2 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,6 +124,7 @@ Hedgehog is a package of agents and skills. An opinionated stack is used so the
124
124
  | Backend | NestJS | Modules naturally mirror Hedgehog's build progression. |
125
125
  | ORM | Drizzle + drizzle-zod | Database schema is the single source of truth. |
126
126
  | Database | PostgreSQL | Simple, relational, predictable. |
127
+ | Local infra | Docker Compose | Postgres/Redis run identically on every machine. |
127
128
  | Platform | Railway | Infrastructure is available from the first commit. |
128
129
  | API contract | ts-rest | Contracts are code, not documentation. |
129
130
  | Validation | Zod | One schema for runtime and compile time. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "0.1.2",
3
+ "version": "0.1.7",
4
4
  "description": "Install the Hedgehog build discipline (agents + skills) into a repo.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -13,6 +13,9 @@
13
13
  "bin": {
14
14
  "hedgehog": "bin/cli.mjs"
15
15
  },
16
+ "scripts": {
17
+ "release": "npm version patch -m \"chore: bump version to %s\""
18
+ },
16
19
  "files": [
17
20
  "bin",
18
21
  "src/agents",
@@ -56,8 +56,13 @@ Revising a draft is a normal edit — the Correction Protocol
56
56
 
57
57
  1. **Scope boundary** — what's in, what's explicitly out.
58
58
  2. **Domain vocabulary** — the nouns and verbs of the problem.
59
- 3. **Screen/flow notes**, when offered captured verbatim by module, for
60
- `ux-planner` to act on at that module's Phase B (see below).
59
+ 3. **`docs/context.md`** the product narrative, scope boundary, and
60
+ domain vocabulary, written as current state (see below). Mandatory,
61
+ every project gets one.
62
+ 4. **Screen/flow notes** — captured by module in
63
+ `docs/design/<module>-notes.md`, for `ux-planner` to act on at that
64
+ module's Phase B (see below). Mandatory per module in scope, even when
65
+ nothing was offered for that module.
61
66
 
62
67
  ### Screens, flows, and other visual input
63
68
 
@@ -68,11 +73,70 @@ here, and doubles as raw material for Phase B later.
68
73
 
69
74
  Layout, styling, and interaction described — "the dashboard should show X
70
75
  and Y together," "this should feel like Stripe's checkout" — are
71
- captured under the relevant module in `docs/design/<module>-notes.md`
72
- (create it if needed). `ux-planner` turns this into a screen rationale
73
- once that module's contract and hook exist, in Phase A build order. Name
74
- this in the moment: "noted for `<module>`'s screen — that gets built
75
- after its API is working."
76
+ captured under the relevant module in `docs/design/<module>-notes.md`.
77
+ `ux-planner` turns this into a screen rationale once that module's
78
+ contract and hook exist, in Phase A build order. Name this in the moment:
79
+ "noted for `<module>`'s screen — that gets built after its API is
80
+ working."
81
+
82
+ Every module in scope gets this file, whether or not anything was
83
+ offered for it — a module with no screen input yet still gets a
84
+ `docs/design/<module>-notes.md` stating that plainly, not a missing
85
+ file.
86
+
87
+ ### Interview formatting
88
+
89
+ The interview is the whole UI — there's no screen to lean on, so
90
+ structure carries the weight a layout normally would. Keep a light,
91
+ consistent set of markers so the person can tell at a glance what kind of
92
+ turn they're reading, without turning the transcript noisy:
93
+
94
+ - A bold section title on entering a new stage (`**Scope**`,
95
+ `**Domain vocabulary**`, `**Confirm & lock**`) — once per stage, not
96
+ once per message.
97
+ - 🧭 for the opening brain-dump prompt, ❓ for a single follow-up
98
+ question, 🔍 for reading supplied material (a screenshot, a
99
+ spreadsheet), 📋 for a synthesis recap, 🔒 for the final confirmation
100
+ gate. One icon per turn, at most — this marks the kind of turn, it
101
+ isn't decoration.
102
+ - Tables for anything tabular (scope boundary, vocabulary) — never
103
+ prose pretending to be a table.
104
+ - Short paragraphs, one question per turn (already required below) —
105
+ never stack multiple asks in one block just because both fit.
106
+
107
+ This applies to every Intake turn from here on, not just the ones
108
+ below.
109
+
110
+ ### Confidence tracking
111
+
112
+ Track a running confidence estimate for "I know what this person
113
+ actually wants" — not "I have enough to start guessing." Confidence
114
+ rises only when an answer resolves genuine ambiguity; a detail that
115
+ just restates something already clear doesn't move it.
116
+
117
+ State it out loud periodically as elicitation progresses — after
118
+ synthesis produces a draft, and any time confidence is low enough that
119
+ continuing to guess would be worse than asking:
120
+
121
+ > Confidence: ~70% — clear on the core flow and the actors, still open
122
+ > on what "cancel" means once payment's been taken, and whether
123
+ > `status` is a single lifecycle or independent tags.
124
+
125
+ Keep asking until confidence reaches **95%**: every module has a
126
+ resolved "owned by," at least one out-of-scope item has surfaced, and
127
+ no term is doing double duty for two different lifecycles (see "When
128
+ to ask instead of guess," below). 95% is a floor on understanding
129
+ what's real, not a ceiling on how much detail to gather — don't pad the
130
+ interview past the point where more questions stop resolving
131
+ ambiguity. If the person answers "I don't know, just pick something,"
132
+ that's a resolved answer (their call, recorded as such) — it moves
133
+ confidence up, not down.
134
+
135
+ Never silently settle for "what I think they should want." If a
136
+ guess would fill a gap, surface it as a candidate to confirm or
137
+ correct instead of writing it into the draft unchallenged — this is
138
+ the same move as "offering a candidate boundary... to confirm or
139
+ correct" below, applied to every gap, not just scope.
76
140
 
77
141
  ### Elicitation — what to ask
78
142
 
@@ -141,10 +205,50 @@ on — read which one this is early and let it set the pace.
141
205
  to.
142
206
  5. **Mark it provisional** — consumed by Bootstrap and revised there or
143
207
  at the schema step as needed.
144
- 6. **File any screen/flow notes** under their module in
145
- `docs/design/<module>-notes.md`, verbatim or lightly organized raw
146
- material for `ux-planner`, not a rationale, so don't polish or
147
- structure beyond attributing it to the right module.
208
+ 6. **Run Confirm & lock** (below) before writing anything. Only after
209
+ the person confirms does synthesis proceed to steps 7–8.
210
+ 7. **Write `docs/context.md`**: product narrative, scope boundary, and
211
+ the domain vocabulary table, stated as current state only — no record
212
+ of alternatives considered, no "originally X, now Y." A later Intake
213
+ updates this file in place so it keeps reading as current state; it
214
+ never grows into a history.
215
+ 8. **File screen/flow notes** under their module in
216
+ `docs/design/<module>-notes.md`, one file per module in scope, even
217
+ when nothing was offered for that module (say so plainly instead of
218
+ omitting the file) — verbatim or lightly organized, raw material for
219
+ `ux-planner`, not a rationale, so don't polish or structure beyond
220
+ attributing it to the right module.
221
+
222
+ ### Confirm & lock
223
+
224
+ Everything above this point is provisional and cheap to change — nothing
225
+ has been written yet. This stage is the last point before that stops
226
+ being true, so it's a hard stop, not a recap in passing.
227
+
228
+ 🔒 **Confirm & lock**. Show, in full, not condensed:
229
+
230
+ - The scope boundary table (in / out).
231
+ - The domain vocabulary table (entity, definition, attributes, owned
232
+ by).
233
+ - The module list in build order, with any cross-module FK dependencies
234
+ flagged.
235
+ - The confidence estimate, and what it's based on (e.g. "95% — every
236
+ module has a resolved owner, one out-of-scope item confirmed, no
237
+ double-duty terms remaining").
238
+
239
+ Then state plainly what happens on confirmation, before it happens:
240
+
241
+ > This locks in `docs/context.md` and the per-module design notes, and
242
+ > starts Phase A build (schema first) on the first module. Anything
243
+ > wrong or missing — say so now; it's a normal edit before this point,
244
+ > and a Correction Protocol entry after. Confirm to proceed, or tell me
245
+ > what to change.
246
+
247
+ Wait for an explicit go-ahead. A revision here is just another
248
+ elicitation turn — update the draft, re-run this stage, don't write
249
+ anything until the confirmation holds. Don't lower the bar because the
250
+ person seems eager to start; the cost of a wrong schema is exactly why
251
+ this gate exists.
148
252
 
149
253
  ### Worked example
150
254
 
@@ -199,15 +303,20 @@ ask now than fix forward later.
199
303
  schema.
200
304
  - Update `TODO.md` to reflect the checklist for what's in scope, mirroring
201
305
  the phase/step structure from `hedgehog-loop`.
306
+ - Write and maintain `docs/context.md` — the product narrative, scope
307
+ boundary, and domain vocabulary, stated as current state only.
308
+ Mandatory on every project; not conditional on domain complexity.
202
309
  - Screens or flows described during Intake are captured under the
203
- relevant module (`docs/design/<module>-notes.md`); Phase B, after the
204
- backend exists for that module, is when they get acted on.
310
+ relevant module (`docs/design/<module>-notes.md`, one per module in
311
+ scope, always present); Phase B, after the backend exists for that
312
+ module, is when they get acted on.
205
313
 
206
314
  ## Workflow
207
315
 
208
316
  1. **Read the requirement** fully before doing anything.
209
- 2. **Check `TODO.md` and the commit log** for what's already built —
210
- `feat(<module>): api` commits mark modules with a closed Phase A.
317
+ 2. **Check `TODO.md`, `docs/context.md`, and the commit log** for what's
318
+ already built — `feat(<module>): api` commits mark modules with a
319
+ closed Phase A.
211
320
  3. **Run Intake** if this is project start: extract scope boundary and
212
321
  domain vocabulary per the procedure above. If input is insufficient,
213
322
  ask — don't guess at scope.
@@ -217,19 +326,35 @@ ask now than fix forward later.
217
326
  referenced by another's FK doesn't need to exist first — FK-by-ID
218
327
  means no compile-time coupling — but flag it if joined reads are
219
328
  expected from day one, since that shapes contract design).
220
- 6. **Write/update `TODO.md`**: a checklist mirroring the Phase A and
329
+ 6. **Run Confirm & lock** (Intake procedure, above) before writing
330
+ anything below. On a project's first Intake this is mandatory; on a
331
+ later Intake adding new scope, re-run it scoped to what's new.
332
+ 7. **Write/update `TODO.md`**: a checklist mirroring the Phase A and
221
333
  Phase B steps per module in scope. Checked or unchecked is its only
222
334
  state. On a second Intake (new scope entering play), append new
223
335
  module sections only — never touch an existing module's checked
224
336
  boxes or reorder modules already in progress.
225
- 7. **File any screen/flow notes** captured during Intake under
226
- `docs/design/<module>-notes.md`, per module.
227
- 8. **Return a summary**: scope boundary, module list, any open questions.
337
+ 8. **Write/update `docs/context.md`**: product narrative, scope boundary,
338
+ domain vocabulary — current state only. On a second Intake, update it
339
+ in place to reflect the new current state; don't append a log of what
340
+ changed or why.
341
+ 9. **File screen/flow notes** captured during Intake under
342
+ `docs/design/<module>-notes.md`, one file per module in scope — create
343
+ it even for a module with no screen input yet, stating that plainly.
344
+ 10. **Return a summary**: scope boundary, module list, any open
345
+ questions.
228
346
 
229
347
  ## Constraints
230
348
 
231
349
  - Never write or modify application code. Read-only against the
232
- codebase; you may write `TODO.md` and `docs/design/<module>-notes.md`.
350
+ codebase; you may write `TODO.md`, `docs/context.md`, and
351
+ `docs/design/<module>-notes.md`.
352
+ - `docs/context.md` and `docs/design/<module>-notes.md` are not
353
+ optional — every project gets the former, every module in scope gets
354
+ the latter, regardless of how much material Intake produced.
355
+ - State current state only in `docs/context.md` — no negation of
356
+ alternatives, no changelog-style narration, no "we used to say X." If
357
+ Intake revises something, edit the file to say what's true now.
233
358
  - Never invent scope. Ambiguous scope means stop and ask.
234
359
  - Don't replan a module's internal step sequence — fixed by
235
360
  `hedgehog-loop`, not a per-project decision.
@@ -29,6 +29,7 @@ One opinionated stack, applied the same way on every project:
29
29
  | Backend framework | NestJS |
30
30
  | ORM | Drizzle (+ `drizzle-zod`) |
31
31
  | Database | PostgreSQL |
32
+ | Local infra | Docker Compose (Postgres + Redis) |
32
33
  | Platform | Railway |
33
34
  | API contract | ts-rest |
34
35
  | Validation | Zod |
@@ -99,6 +100,20 @@ Confirm this hasn't already run: check for an existing Nx workspace
99
100
  existing workspace is a Correction Protocol case (patch the specific
100
101
  config step at its source, per `hedgehog-loop`), not a re-scaffold.
101
102
 
103
+ If a fresh `pnpm install` in the generated workspace fails on a binary
104
+ package's postinstall script with a version-mismatch error (e.g.
105
+ "Expected X but got Y" for a native binary like `esbuild`), don't assume
106
+ project misconfiguration or a corrupted pnpm store — check **Known issue:
107
+ esbuild postinstall version mismatch** below first; this is a known,
108
+ deterministic collision, not something to misdiagnose from scratch.
109
+
110
+ Confirm Docker is available (`docker --version`) before step 1. Local
111
+ Postgres/Redis run through Docker Compose on every host OS — see **Local
112
+ infra: Docker, always** below. No Docker installed: stop and point the
113
+ user to installing Docker Desktop (macOS/Windows) or Docker
114
+ Engine (Linux) rather than falling back to a natively-installed
115
+ Postgres/Redis.
116
+
102
117
  ## Steps (run in sequence, one commit per step)
103
118
 
104
119
  ### 1. Nx workspace + `packages/config`
@@ -119,6 +134,26 @@ pnpm add -D @nx/js
119
134
  one it falls into standalone (`.nx` wrapper) mode instead of a proper
120
135
  pnpm workspace.
121
136
 
137
+ `nx init` does not reliably respect an existing `packageManager: pnpm@...`
138
+ field — it can run its own install via npm regardless, leaving a
139
+ `package-lock.json` next to the intended `pnpm-lock.yaml`. Immediately
140
+ after `nx init` completes, check for `package-lock.json` at the repo
141
+ root; if present, delete it and run `pnpm install` to regenerate
142
+ `pnpm-lock.yaml` before continuing to step 2. Don't assume `nx init`
143
+ respects the locked package manager — verify.
144
+
145
+ `nx init` also does not create `pnpm-workspace.yaml`. Without it, pnpm
146
+ doesn't recognize `packages/*` or `apps/*` as workspace members —
147
+ `pnpm add -w` fails with `ERR_PNPM_ADDING_TO_ROOT`-adjacent errors, and
148
+ cross-package `pnpm add` for a lib silently installs to the wrong place.
149
+ Create it manually right after `nx init`, before the first `pnpm add`:
150
+
151
+ ```yaml
152
+ packages:
153
+ - 'packages/*'
154
+ - 'apps/*'
155
+ ```
156
+
122
157
  Then generate the first lib. The **first** `@nx/js:lib` call materializes
123
158
  the whole workspace shape (`tsconfig.base.json`, root `eslint.config.mjs`,
124
159
  `.prettierrc`, `vitest.workspace.ts`, the `packages/` layout, and the
@@ -135,7 +170,16 @@ files:
135
170
  - `packages/config/eslint-base.js` — flat config, extended by every
136
171
  app/lib. Include `@nx/enforce-module-boundaries` and `depConstraints`
137
172
  from Enforcement wiring below, verbatim.
138
- - `packages/config/prettier.js` — includes `prettier-plugin-tailwindcss`.
173
+ - `packages/config/prettier.js` — **don't** add `prettier-plugin-tailwindcss`
174
+ here. The plugin parses every file prettier touches (not just files with
175
+ Tailwind classes) and throws (`TypeError: e.charAt is not a function` /
176
+ `a.startsWith is not a function`) on any file when no Tailwind config is
177
+ resolvable yet — which is every project from step 1 through step 5, since
178
+ Tailwind doesn't arrive until `apps/web` in step 6. Loading it globally
179
+ here breaks `nx format:write` and any bare `prettier --check` for the
180
+ first five steps of every project. Add the plugin to `apps/web`'s own
181
+ prettier setup (extending this base config) once Tailwind exists, not to
182
+ the shared base.
139
183
  - `packages/config/env.schema.ts` — the Zod env schema from Enforcement
140
184
  wiring below (`DATABASE_URL`, `BETTER_AUTH_SECRET`, `REDIS_URL`,
141
185
  `NODE_ENV`; extend per project as new infra is added later).
@@ -143,6 +187,65 @@ files:
143
187
  declares the project tags table below (`scope:*`, `type:*`) as comments
144
188
  or a lookup, so every later generator step tags its project correctly.
145
189
 
190
+ Root `eslint.config.mjs` is itself ESM (`.mjs`), which surfaces a Node
191
+ warning ("Module type of file... is not specified") unless the root
192
+ `package.json` declares `"type": "module"`. Setting that is correct and
193
+ worth doing — but it changes how Node resolves *every* plain `.js` file
194
+ in the repo from CommonJS to ESM by default. Several files later steps
195
+ generate are CommonJS (`require`/`module.exports`) and will break with
196
+ `ReferenceError: require is not defined` or `module is not defined` the
197
+ moment `"type": "module"` is set: `apps/*/webpack.config.js`,
198
+ `commitlint.config.js`, and any hand-written CommonJS tool script (e.g.
199
+ `tools/phase-gate.js`). Rename each to `.cjs` as it's created (or convert
200
+ its content to ESM, as `next.config.js` supports natively via
201
+ `export default`) rather than discovering the break later when `nx
202
+ graph` or a generator that depends on the project graph fails
203
+ opaquely — Nx surfaces this as "Failed to process project graph" pointing
204
+ at the offending file, not as a module-system explanation.
205
+
206
+ Add a root `docker-compose.yml` provisioning Postgres and Redis for local
207
+ dev, regardless of host OS (macOS, Windows, Linux) — see **Local infra:
208
+ Docker, always** below for why this isn't optional. `DATABASE_URL` and
209
+ `REDIS_URL` in `.env` point at the compose services from the first
210
+ commit, matching the env schema below.
211
+
212
+ ```yaml
213
+ services:
214
+ postgres:
215
+ image: postgres:16-alpine
216
+ environment:
217
+ POSTGRES_USER: postgres
218
+ POSTGRES_PASSWORD: postgres
219
+ POSTGRES_DB: app
220
+ ports:
221
+ - '5432:5432'
222
+ volumes:
223
+ - postgres-data:/var/lib/postgresql/data
224
+
225
+ redis:
226
+ image: redis:7-alpine
227
+ ports:
228
+ - '6379:6379'
229
+ volumes:
230
+ - redis-data:/data
231
+
232
+ volumes:
233
+ postgres-data:
234
+ redis-data:
235
+ ```
236
+
237
+ Add an `esbuild` override to root `package.json` in this step, before step
238
+ 2 installs `drizzle-kit` — see **Known issue: esbuild postinstall version
239
+ mismatch** below for why.
240
+
241
+ ```json
242
+ {
243
+ "pnpm": {
244
+ "overrides": { "esbuild": "0.25.12" }
245
+ }
246
+ }
247
+ ```
248
+
146
249
  Commit: `feat(config): workspace + shared config`
147
250
 
148
251
  ### 2. `packages/db` — Drizzle client + connection
@@ -182,6 +285,20 @@ structured logging. Call `loadEnv()` at the top of `apps/api/src/main.ts`.
182
285
  Tag: `scope:api`. No controllers beyond a health check — domain
183
286
  controllers arrive per module in Phase A.
184
287
 
288
+ `@nx/nest:app` also scaffolds a companion `apps/api-e2e` project wired to
289
+ Jest (`jest.config.cts`), not Vitest — inconsistent with the locked stack.
290
+ Convert it: delete the Jest config, add a `vitest.config.mts` (mirroring
291
+ `packages/db`'s), and give `apps/api-e2e`'s `tsconfig.spec.json` the same
292
+ `composite`/`declaration` treatment as above. Rename its target from the
293
+ plugin-inferred `test` to an explicit `e2e` — an HTTP e2e suite needs a
294
+ live server (`dependsOn: ["api:build", "api:serve"]`), and if it keeps the
295
+ default `test` name, `nx affected -t test` (what lefthook's pre-commit
296
+ hook runs on every commit, per the Commit gate below) will try to boot a
297
+ server on every commit. Exclude `apps/api-e2e` from the `@nx/vitest`
298
+ plugin's auto-inference in `nx.json` (`"exclude": ["apps/api-e2e/**"]` on
299
+ that plugin entry) so it stops registering a `test` target for this
300
+ project at all, keeping only the manually-defined `e2e` target.
301
+
185
302
  Commit: `feat(api): nest shell + global guard + pino`
186
303
 
187
304
  ### 5. `apps/worker` — BullMQ seam (Redis, no consumers yet)
@@ -203,17 +320,73 @@ Commit: `feat(worker): bullmq seam, no consumers`
203
320
  ```bash
204
321
  npx nx g @nx/next:app apps/web
205
322
  pnpm add @tanstack/react-query
206
- pnpm dlx shadcn@latest init
207
323
  ```
208
324
 
209
- Wire the TanStack Query provider at the root layout. `shadcn init` writes
210
- `apps/web`'s base theme (CSS variables for color, radius, light/dark
211
- mode) set the actual palette here, once, rather than leaving ShadCN's
212
- placeholder values for `ui-builder` to inherit unnoticed on the first
213
- screen. Light/dark mode toggle wiring belongs here too, not as a
214
- per-screen decision later. No screens or hooks yet Phase B doesn't
325
+ **Do not run `pnpm dlx shadcn@latest init` (or `shadcn add`) against
326
+ `apps/web` directly.** The shadcn CLI hard-requires a `package.json` in
327
+ its target directory to detect the project; an Nx-generated app under
328
+ this stack has no per-app `package.json` (dependencies live at the
329
+ workspace root). Finding none, the CLI's default behavior is to offer
330
+ and on `--yes`, silently proceed to scaffold a *brand-new*
331
+ `create-next-app` project **inside** `apps/web`, complete with its own
332
+ nested `.git`, its own lockfile, and its own `package.json`. This doesn't
333
+ error; it succeeds and leaves a corrupted nested project (e.g.
334
+ `apps/web/web/`) that has to be manually detected and deleted. There is
335
+ currently no supported non-interactive flag that makes shadcn's CLI
336
+ target an existing package-json-less directory in place.
337
+
338
+ Build the base theme by hand instead: write `apps/web/components.json`
339
+ directly (style, aliases, `tailwind.css` path — the `css` field), add
340
+ `class-variance-authority`, `clsx`, `tailwind-merge`, `lucide-react`, and
341
+ `@radix-ui/react-slot` as dependencies, write `apps/web/src/lib/utils.ts`
342
+ (the standard `cn()` helper), and hand-write the CSS variable theme block
343
+ (light/dark, using shadcn's published default token values) into
344
+ `apps/web`'s global stylesheet. Individual components (e.g. `button.tsx`)
345
+ can then be hand-written from shadcn's published source for that
346
+ component — small, stable, well-known files — rather than fetched via the
347
+ CLI. Set the actual palette here, once, rather than leaving placeholder
348
+ values for `ui-builder` to inherit unnoticed on the first screen. Light/
349
+ dark mode toggle wiring belongs here too, not as a per-screen decision
350
+ later.
351
+
352
+ Wire the TanStack Query provider at the root layout (App Router: a
353
+ `'use client'` `Providers` wrapper component, since the root layout
354
+ itself is a server component). No screens or hooks yet — Phase B doesn't
215
355
  start until Phase A closes for at least one module. Tag: `scope:web`.
216
356
 
357
+ **Set `NODE_ENV` explicitly on the `build` target, or the production
358
+ build silently runs in dev mode under Nx.** Nx's task runner sets
359
+ `NODE_ENV=development` by default for every task unless a target
360
+ overrides it — including a plain `nx:run-commands` target running `next
361
+ build`. Next.js's production build pipeline assumes `NODE_ENV=production`;
362
+ running it under `development` produces a build that compiles and
363
+ appears to succeed on individual pages but crashes prerendering the
364
+ auto-generated `/_global-error` route with `TypeError: Cannot read
365
+ properties of null (reading 'useContext')` — a React-internals mismatch
366
+ from the dev/prod build split, not an app bug. This only reproduces
367
+ through `nx run web:build`; the identical `next build --webpack` run
368
+ directly from `apps/web` succeeds, because a bare shell has no `NODE_ENV`
369
+ set and Next defaults it correctly itself — which makes the symptom look
370
+ Nx-specific and environment-related rather than what it is (a task-runner
371
+ default silently overriding a value the build assumes). Set it on the
372
+ `build` target only, in `apps/web/project.json`:
373
+
374
+ ```json
375
+ {
376
+ "targets": {
377
+ "build": {
378
+ "options": {
379
+ "command": "next build --webpack",
380
+ "env": { "NODE_ENV": "production" }
381
+ }
382
+ }
383
+ }
384
+ }
385
+ ```
386
+
387
+ Leave `dev` alone — it correctly wants `NODE_ENV=development`, which is
388
+ also what Nx already defaults it to.
389
+
217
390
  Commit: `feat(web): next shell + query provider + base theme`
218
391
 
219
392
  ### 7. `apps/mobile` — Expo shell (only if mobile is in scope)
@@ -288,6 +461,51 @@ e.g. `libs/orders/port`, `libs/orders/repository`, `libs/orders/service`.
288
461
  Building out of order (a controller before a service exists, a hook
289
462
  reaching into `apps/api` directly) fails `nx lint`.
290
463
 
464
+ The rule list above is illustrative, not exhaustive — `@nx/enforce-
465
+ module-boundaries` denies by default for any project whose tag isn't
466
+ named as a `sourceTag` in some `depConstraints` entry ("A project without
467
+ tags matching at least one constraint cannot depend on any libraries").
468
+ `packages/db` (`scope:db`, `type:adapter`) and `apps/api` (`scope:api`)
469
+ both hit this the moment they're generated and tagged in steps 2 and 4,
470
+ because neither `type:adapter` nor `scope:api` has an entry above. Add
471
+ constraints for every tag combination as it's introduced, not only the
472
+ ones in the illustrative list — at minimum `type:adapter` (needs
473
+ `type:util`, for `packages/config`) and `scope:api` (needs `type:port`,
474
+ `type:service`, `type:util` — never `scope:db` directly; `apps/api`
475
+ reaches storage through a module's repository/service, not by importing
476
+ `packages/db` itself).
477
+
478
+ ### Typecheck target (required before the commit gate can work)
479
+
480
+ `lefthook.yml`'s pre-commit below runs `nx affected -t typecheck`, but
481
+ `@nx/js:lib` and `@nx/nest:app` don't register a `typecheck` target on
482
+ their own — it only exists once the `@nx/js/typescript` plugin is
483
+ registered in `nx.json`, added in step 1 alongside the eslint and
484
+ vitest plugins:
485
+
486
+ ```json
487
+ {
488
+ "plugin": "@nx/js/typescript",
489
+ "options": { "typecheck": { "targetName": "typecheck" } }
490
+ }
491
+ ```
492
+
493
+ Registering the plugin isn't sufficient on its own, either: `tsc
494
+ --build`'s composite-project mode (what the inferred `typecheck` target
495
+ actually runs) requires every `tsconfig.lib.json` and `tsconfig.spec.json`
496
+ it touches to set `"composite": true` and `"declaration": true`. The
497
+ generators don't set these — every `@nx/js:lib` and `@nx/nest:app`
498
+ project needs both added by hand to `tsconfig.lib.json` /
499
+ `tsconfig.app.json` and `tsconfig.spec.json` right after generation, or
500
+ `nx run <proj>:typecheck` fails immediately with `TS5069: Option
501
+ 'emitDeclarationOnly' cannot be specified without specifying option
502
+ 'declaration' or option 'composite'`. If a project's spec file imports
503
+ from its own lib source (the common case), `tsconfig.spec.json` also
504
+ needs an explicit `references` entry pointing at `tsconfig.lib.json`, or
505
+ `tsc` reports `TS6307: File '...' is not listed within the file list of
506
+ project`. Do this for `packages/config` in step 1 so the pattern is
507
+ established before every later step repeats it.
508
+
291
509
  ### Commit gate (lefthook + commitlint)
292
510
 
293
511
  Enforces one coherent change, tested, conventionally committed. Runs on
@@ -301,13 +519,13 @@ pre-commit:
301
519
  commands:
302
520
  typecheck:
303
521
  glob: "*.{ts,tsx}"
304
- run: npx nx affected -t typecheck --files={staged_files}
522
+ run: npx nx affected -t typecheck --base=HEAD
305
523
  lint:
306
524
  glob: "*.{ts,tsx}"
307
- run: npx nx affected -t lint --files={staged_files}
525
+ run: npx nx affected -t lint --base=HEAD
308
526
  test:
309
527
  glob: "*.{ts,tsx}"
310
- run: npx nx affected -t test --files={staged_files}
528
+ run: npx nx affected -t test --base=HEAD
311
529
 
312
530
  commit-msg:
313
531
  commands:
@@ -315,6 +533,14 @@ commit-msg:
315
533
  run: npx commitlint --edit {1}
316
534
  ```
317
535
 
536
+ Don't pass `--files={staged_files}` to `nx affected` — `nx affected`
537
+ forwards unrecognized args straight through to the underlying target
538
+ command (e.g. `eslint .`), so `--files={staged_files}` becomes
539
+ `eslint . <path>` and errors on any path that doesn't match eslint's own
540
+ glob expectations. `--base=HEAD` (comparing against the last commit) is
541
+ what makes `nx affected` scope correctly to a pre-commit hook's staged
542
+ changes.
543
+
318
544
  **`commitlint.config.js`:**
319
545
 
320
546
  ```js
@@ -335,6 +561,14 @@ exists once it compiles and passes.
335
561
  Run `pnpm dlx lefthook install` once `lefthook.yml` exists so the gate is
336
562
  active from step 2 onward.
337
563
 
564
+ A machine with lefthook installed globally (e.g. via Homebrew) on `PATH`
565
+ can have that version picked up by the git hook shim instead of the
566
+ project's pinned local one, silently running different — possibly
567
+ incompatible — behavior. After `lefthook install`, verify the hook is
568
+ invoking the local pinned version (check `lefthook version` output during
569
+ a commit, or that the hook script under `.git/hooks/` resolves to
570
+ `node_modules/.bin/lefthook`) rather than a global shadow.
571
+
338
572
  ### Env validation (fail fast)
339
573
 
340
574
  Types-first extended to config. Boot fails immediately on a missing or
@@ -398,10 +632,58 @@ One shared config, extended everywhere:
398
632
 
399
633
  - `packages/config/eslint-base.js` — flat config, extended by every
400
634
  app/lib.
401
- - `packages/config/prettier.js` — includes `prettier-plugin-tailwindcss`.
635
+ - `packages/config/prettier.js` — the shared base, *without*
636
+ `prettier-plugin-tailwindcss` (see step 1 — it belongs in `apps/web`'s
637
+ own config once Tailwind exists, not the shared base).
402
638
 
403
639
  A per-app override request signals to fix the base config at the source.
404
640
 
641
+ ## Local infra: Docker, always
642
+
643
+ Postgres and Redis run through the `docker-compose.yml` from step 1 on
644
+ every project, on every host OS — macOS, Windows, Linux alike. This isn't
645
+ a convenience default; it's what makes "clone the repo, run the stack"
646
+ mechanically true regardless of who's building. A natively-installed
647
+ Postgres or Redis (Homebrew, an existing Windows service, a system
648
+ package) is a per-machine setup step that isn't in the commit history and
649
+ isn't reproducible on the next machine — exactly the kind of
650
+ tribal-knowledge dependency Hedgehog's enforcement exists to remove.
651
+
652
+ Don't offer a "native install" path as an alternative, even if a
653
+ contributor already has Postgres running locally for another project. One
654
+ mechanism, every machine: `docker compose up -d` before `pnpm install`,
655
+ every time. If Docker genuinely can't run on a target machine, that's a
656
+ platform-support gap to raise, not a reason to quietly fall back to a
657
+ native install for that one contributor.
658
+
659
+ ## Known issue: esbuild postinstall version mismatch
660
+
661
+ `@nx/vite` (step 1) declares `esbuild` as an *optional* peer dependency
662
+ (`^0.27.0 || ^0.28.0`), while `drizzle-kit` (step 2) pins a hard dependency
663
+ on `esbuild@^0.25.4`. pnpm's isolated store correctly keeps both esbuild
664
+ majors side by side — but esbuild's own `install.js` resolves its platform
665
+ binary (`@esbuild/<platform>`) via **ambient** Node module resolution
666
+ rather than a path scoped to its own package instance. With multiple
667
+ esbuild majors in the tree, that ambient resolution can walk up and grab a
668
+ sibling major's platform binary, hardlinking the wrong version's binary
669
+ into a package that still claims a different version number. The result
670
+ is a postinstall failure like:
671
+
672
+ ```
673
+ Error: Expected "0.28.1" but got "0.25.12"
674
+ ```
675
+
676
+ This is deterministic (not registry/store corruption) and reproduces even
677
+ from a fully clean pnpm store — it's a real collision in esbuild's install
678
+ script when it meets pnpm's multi-version isolation. The fix is the
679
+ `pnpm.overrides.esbuild` pin added in step 1 above: collapsing to a single
680
+ esbuild version (drizzle-kit's hard-pinned range, since it's non-optional
681
+ — `@nx/vite`'s peer is optional and simply goes unfilled) removes the
682
+ ambiguity that triggers the bug. If this resurfaces after a stack version
683
+ bump, re-check drizzle-kit's current `esbuild` dependency range
684
+ (`pnpm view drizzle-kit dependencies.esbuild`) and update the override to
685
+ match rather than removing it.
686
+
405
687
  ## After Bootstrap
406
688
 
407
689
  Update `TODO.md`: check off every Bootstrap line now built, leave Phase
@@ -423,3 +705,7 @@ gated by lefthook, each its own commit.
423
705
  - Each of the 7 steps is its own commit, in order — same unit-of-work
424
706
  discipline as every other step in the discipline, even though this is
425
707
  infra rather than a domain module.
708
+ - Local Postgres/Redis always run through the `docker-compose.yml` from
709
+ step 1, on every host OS. Never substitute a natively-installed
710
+ Postgres/Redis, even to match a contributor's existing local setup —
711
+ see **Local infra: Docker, always**.
@@ -13,8 +13,8 @@
13
13
 
14
14
  {{PROJECT_SUMMARY — 2–4 sentences the `planner` writes at Intake: what
15
15
  this project is, who it's for, and what it does. State current intent, not
16
- history. Keep it tight — deeper domain context lives in the commit log and
17
- docs/design, not here.}}
16
+ history. Keep it tight — the full product narrative, scope boundary, and
17
+ domain vocabulary live in docs/context.md, not here.}}
18
18
 
19
19
  This project is built with **Hedgehog**: a backend-first, one-step-at-a-time
20
20
  build discipline. The rules below aren't project preferences — they're how
@@ -28,6 +28,10 @@ whole plan in context — the plan lives in the structure:
28
28
  - **`TODO.md`** is the live checklist and the source of truth for what's
29
29
  next. Read it at the start of every session. Its only state is
30
30
  checked/unchecked.
31
+ - **`docs/context.md`** is the product's current-state document — product
32
+ narrative, scope boundary, domain vocabulary. Every project has one,
33
+ written by `planner` at Intake and kept current on later Intakes. It
34
+ states what's true now, never a history of what changed.
31
35
  - **The commit log** is the record of what's built and why. Conventional
32
36
  commits (`feat(<module>): schema`, `feat(<module>): api`, …) are how
33
37
  progress is read, not a conversation summary.
@@ -63,8 +67,8 @@ steps from memory:
63
67
  ### The agents — delegate the judgment calls
64
68
 
65
69
  - **`planner`** — Intake (scope boundary + domain vocabulary) at project
66
- start, and module scoping when new scope enters play. Writes `TODO.md`
67
- and `docs/design/<module>-notes.md`.
70
+ start, and module scoping when new scope enters play. Writes `TODO.md`,
71
+ `docs/context.md`, and `docs/design/<module>-notes.md`.
68
72
  - **`ux-planner`** — once per module in Phase B, after the hook exists and
69
73
  before the screen: writes `docs/design/<module>.md`.
70
74
  - **`ui-builder`** — builds screens from the ux-planner rationale.
@@ -77,11 +81,13 @@ steps from memory:
77
81
  ### Stack (locked)
78
82
 
79
83
  Nx monorepo · pnpm · **NestJS** (all domain logic + DB access) · **Drizzle**
80
- (+ `drizzle-zod`) · **PostgreSQL** · Railway · **ts-rest** contracts · **Zod**
81
- validation · **Better Auth** · **TanStack Query** hooks · **Next.js** + ShadCN
82
- + Tailwind (web, UI only) · Expo + React Native Reusables + NativeWind
83
- (mobile, optional) · **BullMQ + Redis** (queues) · Pino logging · Vitest +
84
- Playwright (tests) · Conventional Commits + commitlint + lefthook · Sentry.
84
+ (+ `drizzle-zod`) · **PostgreSQL** · **Docker Compose** (local Postgres +
85
+ Redis, every host OS) · Railway · **ts-rest** contracts · **Zod**
86
+ validation · **Better Auth** · **TanStack Query** hooks · **Next.js** +
87
+ ShadCN + Tailwind (web, UI only) · Expo + React Native Reusables +
88
+ NativeWind (mobile, optional) · **BullMQ + Redis** (queues) · Pino logging ·
89
+ Vitest + Playwright (tests) · Conventional Commits + commitlint + lefthook ·
90
+ Sentry.
85
91
 
86
92
  Don't substitute libraries. If a package or generator name changed
87
93
  upstream, verify against current docs before running — don't swap in a
@@ -90,6 +96,7 @@ different library.
90
96
  ### Layout
91
97
 
92
98
  ```
99
+ docker-compose.yml local Postgres + Redis — every host OS, no native install
93
100
  apps/
94
101
  web Next.js — UI only
95
102
  mobile Expo — optional
@@ -106,6 +113,7 @@ packages/
106
113
  libs/
107
114
  <module>/port · <module>/repository · <module>/service (one triplet per table)
108
115
  docs/
116
+ context.md product narrative, scope boundary, domain vocabulary (Intake)
109
117
  design <module>-notes.md (Intake) and <module>.md (ux-planner)
110
118
  ```
111
119
 
@@ -125,6 +133,9 @@ docs/
125
133
  (lefthook gate).
126
134
  - **Fix wrong steps at the source** via the Correction Protocol — never a
127
135
  downstream workaround.
136
+ - **Local Postgres/Redis always run through `docker-compose.yml`**, on
137
+ every host OS. Never a natively-installed Postgres/Redis, even to match
138
+ a contributor's existing local setup.
128
139
  - **`packages/config` is the single source** for shared config. A per-app
129
140
  override request means fix the base config, not add an override.
130
141
 
@@ -147,7 +158,8 @@ off. Keep it thin.
147
158
  **When the build is done:** once every module in scope has both phases
148
159
  checked, the build session is complete. **Delete `TODO.md`** — a finished
149
160
  checklist is noise, and the commit log is the durable record of what was
150
- built.
161
+ built. **`docs/context.md` stays** — it's the product's current-state
162
+ document, not a checklist.
151
163
 
152
164
  ## Managing context
153
165
 
@@ -159,8 +171,9 @@ context small:
159
171
  `TODO.md` and continue. Nothing is lost, because the checklist, commits,
160
172
  and code hold all the state. Prefer this over letting one session
161
173
  accumulate the entire project.
162
- - **A cleared or new session recovers by reading `TODO.md` + the commit
163
- log**, never by needing the prior conversation.
174
+ - **A cleared or new session recovers by reading `TODO.md`,
175
+ `docs/context.md`, and the commit log**, never by needing the prior
176
+ conversation.
164
177
  - **Delegate heavy work to agents.** Intake elicitation (`planner`),
165
178
  screen builds (`ui-builder`), and reviews (`reviewer`) each run in their
166
179
  own isolated context — so that work doesn't pile up in the main thread.
@@ -1,16 +1,16 @@
1
1
  # TODO
2
2
 
3
- <!-- 2-3 sentences: what is this project. Link out to a deeper context
4
- file (e.g. docs/context.md) only if the domain genuinely needs it — most
5
- projects shouldn't need one. -->
3
+ <!-- 2-3 sentences: what is this project. Full product narrative, scope
4
+ boundary, and domain vocabulary live in docs/context.md every project
5
+ has one, written by planner at Intake. -->
6
6
 
7
7
  ## Context
8
8
 
9
- (fill in per project)
9
+ (fill in per project — see docs/context.md for the full picture)
10
10
 
11
11
  ## Bootstrap
12
12
 
13
- - [ ] Nx workspace + `packages/config`
13
+ - [ ] Nx workspace + `packages/config` (incl. `docker-compose.yml` for local Postgres/Redis)
14
14
  - [ ] `packages/db` — Drizzle client
15
15
  - [ ] `packages/auth` — Better Auth config
16
16
  - [ ] `apps/api` — Nest shell, global guard, Pino