@skyf0xx/hedgehog-core-full-stack-app 1.0.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 (137) hide show
  1. package/CLAUDE.core.md +147 -0
  2. package/LICENSE +21 -0
  3. package/README.md +41 -0
  4. package/agents/backend-eng.md +189 -0
  5. package/agents/front-end-eng.md +176 -0
  6. package/agents/ux-planner.md +174 -0
  7. package/hedgehog-core.yaml +20 -0
  8. package/package.json +41 -0
  9. package/skills/hedgehog-bootstrap/SKILL.md +324 -0
  10. package/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +411 -0
  11. package/skills/hedgehog-loop/SKILL.md +643 -0
  12. package/skills/link-workspace-packages/SKILL.md +41 -0
  13. package/skills/nx-generate/SKILL.md +165 -0
  14. package/skills/nx-run-tasks/SKILL.md +52 -0
  15. package/skills/nx-workspace/SKILL.md +261 -0
  16. package/skills/nx-workspace/references/AFFECTED.md +27 -0
  17. package/vendor-skills/GSAP/ATTRIBUTION.md +30 -0
  18. package/vendor-skills/GSAP/LICENSE +21 -0
  19. package/vendor-skills/GSAP/gsap-core/SKILL.md +254 -0
  20. package/vendor-skills/GSAP/gsap-frameworks/SKILL.md +266 -0
  21. package/vendor-skills/GSAP/gsap-performance/SKILL.md +79 -0
  22. package/vendor-skills/GSAP/gsap-plugins/SKILL.md +433 -0
  23. package/vendor-skills/GSAP/gsap-react/SKILL.md +136 -0
  24. package/vendor-skills/GSAP/gsap-scrolltrigger/SKILL.md +296 -0
  25. package/vendor-skills/GSAP/gsap-timeline/SKILL.md +107 -0
  26. package/vendor-skills/GSAP/gsap-utils/SKILL.md +284 -0
  27. package/vendor-skills/GSAP/llms.txt +39 -0
  28. package/workspace/.env.example +5 -0
  29. package/workspace/.github/workflows/phase-gate.yml +17 -0
  30. package/workspace/.prettierignore +5 -0
  31. package/workspace/.prettierrc +3 -0
  32. package/workspace/.vscode/extensions.json +3 -0
  33. package/workspace/apps/api/eslint.config.mjs +3 -0
  34. package/workspace/apps/api/package.json +117 -0
  35. package/workspace/apps/api/src/app/app.module.spec.ts +15 -0
  36. package/workspace/apps/api/src/app/app.module.ts +15 -0
  37. package/workspace/apps/api/src/app/feature-modules.ts +8 -0
  38. package/workspace/apps/api/src/app/health.controller.ts +9 -0
  39. package/workspace/apps/api/src/assets/.gitkeep +0 -0
  40. package/workspace/apps/api/src/main.ts +32 -0
  41. package/workspace/apps/api/tsconfig.app.json +22 -0
  42. package/workspace/apps/api/tsconfig.json +13 -0
  43. package/workspace/apps/api/tsconfig.spec.json +36 -0
  44. package/workspace/apps/api/vitest.config.mts +18 -0
  45. package/workspace/apps/api/webpack.config.cjs +25 -0
  46. package/workspace/apps/api-e2e/eslint.config.mjs +3 -0
  47. package/workspace/apps/api-e2e/package.json +28 -0
  48. package/workspace/apps/api-e2e/src/api/api.spec.ts +11 -0
  49. package/workspace/apps/api-e2e/src/support/global-setup.ts +17 -0
  50. package/workspace/apps/api-e2e/src/support/test-setup.ts +6 -0
  51. package/workspace/apps/api-e2e/tsconfig.json +10 -0
  52. package/workspace/apps/api-e2e/tsconfig.spec.json +28 -0
  53. package/workspace/apps/api-e2e/vitest.config.mts +20 -0
  54. package/workspace/apps/web/.env.example +12 -0
  55. package/workspace/apps/web/.prettierrc.js +10 -0
  56. package/workspace/apps/web/.swcrc +30 -0
  57. package/workspace/apps/web/components.json +21 -0
  58. package/workspace/apps/web/eslint.config.mjs +12 -0
  59. package/workspace/apps/web/index.d.ts +7 -0
  60. package/workspace/apps/web/next-env.d.ts +7 -0
  61. package/workspace/apps/web/next.config.js +9 -0
  62. package/workspace/apps/web/package.json +38 -0
  63. package/workspace/apps/web/postcss.config.mjs +7 -0
  64. package/workspace/apps/web/public/.gitkeep +0 -0
  65. package/workspace/apps/web/public/favicon.ico +0 -0
  66. package/workspace/apps/web/src/app/global.css +83 -0
  67. package/workspace/apps/web/src/app/layout.tsx +34 -0
  68. package/workspace/apps/web/src/app/module-routes.ts +13 -0
  69. package/workspace/apps/web/src/app/page.tsx +34 -0
  70. package/workspace/apps/web/src/app/providers.tsx +12 -0
  71. package/workspace/apps/web/src/components/theme-toggle.spec.tsx +20 -0
  72. package/workspace/apps/web/src/components/theme-toggle.tsx +32 -0
  73. package/workspace/apps/web/src/components/ui/button.tsx +60 -0
  74. package/workspace/apps/web/src/lib/utils.ts +6 -0
  75. package/workspace/apps/web/src/test-setup.ts +1 -0
  76. package/workspace/apps/web/tsconfig.json +62 -0
  77. package/workspace/apps/web/tsconfig.spec.json +37 -0
  78. package/workspace/apps/web/vitest.config.mts +27 -0
  79. package/workspace/apps/web-e2e/eslint.config.mjs +12 -0
  80. package/workspace/apps/web-e2e/package.json +10 -0
  81. package/workspace/apps/web-e2e/playwright.config.mts +75 -0
  82. package/workspace/apps/web-e2e/src/example.spec.ts +7 -0
  83. package/workspace/apps/web-e2e/tsconfig.json +26 -0
  84. package/workspace/commitlint.config.cjs +3 -0
  85. package/workspace/core.yaml +68 -0
  86. package/workspace/docker-compose.yml +14 -0
  87. package/workspace/eslint.config.mjs +15 -0
  88. package/workspace/gitignore.template +24 -0
  89. package/workspace/lefthook.yml +52 -0
  90. package/workspace/nx.json +90 -0
  91. package/workspace/package.json +99 -0
  92. package/workspace/packages/config/eslint-base.js +205 -0
  93. package/workspace/packages/config/package.json +28 -0
  94. package/workspace/packages/config/prettier.js +13 -0
  95. package/workspace/packages/config/src/env.schema.spec.ts +38 -0
  96. package/workspace/packages/config/src/env.schema.ts +18 -0
  97. package/workspace/packages/config/src/index.ts +1 -0
  98. package/workspace/packages/config/tsconfig.json +13 -0
  99. package/workspace/packages/config/tsconfig.lib.json +27 -0
  100. package/workspace/packages/config/tsconfig.spec.json +34 -0
  101. package/workspace/packages/config/vitest.config.mts +18 -0
  102. package/workspace/packages/db/package.json +27 -0
  103. package/workspace/packages/db/src/index.ts +2 -0
  104. package/workspace/packages/db/src/lib/db.spec.ts +48 -0
  105. package/workspace/packages/db/src/lib/db.ts +54 -0
  106. package/workspace/packages/db/src/schema/index.ts +4 -0
  107. package/workspace/packages/db/tsconfig.json +13 -0
  108. package/workspace/packages/db/tsconfig.lib.json +27 -0
  109. package/workspace/packages/db/tsconfig.spec.json +34 -0
  110. package/workspace/packages/db/vitest.config.mts +18 -0
  111. package/workspace/pnpm-lock.yaml +23719 -0
  112. package/workspace/pnpm-workspace.yaml +7 -0
  113. package/workspace/tools/generate-feature-modules.cjs +104 -0
  114. package/workspace/tools/generate-module-routes.cjs +89 -0
  115. package/workspace/tools/generators/contract/generator.ts +336 -0
  116. package/workspace/tools/generators/contract/schema.json +24 -0
  117. package/workspace/tools/generators/controller/generator.ts +390 -0
  118. package/workspace/tools/generators/controller/schema.json +24 -0
  119. package/workspace/tools/generators/fields.ts +160 -0
  120. package/workspace/tools/generators/generators.json +42 -0
  121. package/workspace/tools/generators/hook/generator.ts +291 -0
  122. package/workspace/tools/generators/hook/schema.json +19 -0
  123. package/workspace/tools/generators/lib-shell.ts +124 -0
  124. package/workspace/tools/generators/naming.ts +84 -0
  125. package/workspace/tools/generators/package.json +6 -0
  126. package/workspace/tools/generators/repository/generator.ts +298 -0
  127. package/workspace/tools/generators/repository/schema.json +15 -0
  128. package/workspace/tools/generators/schema/generator.ts +169 -0
  129. package/workspace/tools/generators/schema/schema.json +20 -0
  130. package/workspace/tools/generators/screen/generator.ts +218 -0
  131. package/workspace/tools/generators/screen/schema.json +15 -0
  132. package/workspace/tools/generators/service/generator.ts +270 -0
  133. package/workspace/tools/generators/service/schema.json +19 -0
  134. package/workspace/tools/phase-gate.cjs +77 -0
  135. package/workspace/tsconfig.base.json +21 -0
  136. package/workspace/tsconfig.json +25 -0
  137. package/workspace/vitest.workspace.ts +4 -0
@@ -0,0 +1,643 @@
1
+ ---
2
+ name: hedgehog-loop
3
+ description: Use for every unit of work once a Hedgehog project is bootstrapped — building one layer (schema, contract, repository, service, controller, hook, screen) per module, gated by `hedgehog verify` and committed one layer at a time. Triggers on "next step", "build this module", "what's next", or the start of any work session on a bootstrapped project. Also covers the Correction Protocol for fixing a wrong upstream step.
4
+ ---
5
+
6
+ # Hedgehog Loop
7
+
8
+ The operating loop for a bootstrapped Hedgehog project: `hedgehog claim`
9
+ reserves the packet(s) for ready layers, build them, `hedgehog verify`
10
+ gates and commits each. The build graph (`.hedgehog/hedgehog.db`) is the
11
+ live list — query it via `hedgehog status`/`hedgehog ready`, never
12
+ re-derive state from prose. The step tables below mirror this core's
13
+ `workspace/core.yaml`, the design source of truth
14
+ for layer order, scope, and verify command per layer — read the tables
15
+ for the human-readable shape, and the YAML when they seem to disagree.
16
+
17
+ The packet, though, is what actually runs. `hedgehog plan` copies each
18
+ layer's scope globs, verify command and commit message onto every task
19
+ row at compile time; from then on the row — not `core.yaml` — is what
20
+ `hedgehog claim` hands out and `hedgehog verify` gates against, and
21
+ editing `core.yaml` afterwards does not reach tasks already compiled (a
22
+ plain `hedgehog plan` re-run won't apply it either — it only reads
23
+ intents still pending). `hedgehog status` prints a **DRIFT** section
24
+ whenever the two have diverged, and `hedgehog plan --recompile` rewrites
25
+ the layer-derived fields on not-yet-started tasks from the current
26
+ `core.yaml`, refusing — and naming — every task already building,
27
+ verifying, complete, or blocked. Never patch a task row in SQLite by
28
+ hand: the DB is derived and gitignored, so `hedgehog db rebuild` drops
29
+ the patch.
30
+
31
+ ## Determine phase
32
+
33
+ Before touching code, know which phase applies to the module in scope:
34
+
35
+ - **Phase A** — building/extending the backend. Every module in scope
36
+ needs schema → contract → repository → service → controller before
37
+ Phase B starts for any of them.
38
+ - **Phase B** — Phase A is closed for the module. Build hooks and screens.
39
+
40
+ Check `hedgehog status` (or `hedgehog why <path>` for a specific file),
41
+ or the commit log for `feat(<module>): api` commits. No such commit (and
42
+ no `controller` task `complete` for that module) means the module is in
43
+ Phase A.
44
+
45
+ ## The Domain Module Pattern
46
+
47
+ A **domain module = one table.** `users`, `orders`, `order_items` are each
48
+ their own module, carrying the full step sequence below. The schema is the
49
+ source of truth for module boundaries.
50
+
51
+ **Cross-module references are FK-by-ID only.** If `orders.user_id`
52
+ references `users`, the `orders` schema holds a plain FK column. The
53
+ `orders` repository and service depend only on their own ports — a service
54
+ knows related entities only as an ID.
55
+
56
+ - Need the related row? Resolve it at the contract/controller layer
57
+ (parallel calls to each module's own endpoint), or join against the
58
+ other module's *schema* directly inside the repository (Drizzle query).
59
+ - This keeps every service importing only its own module's port, which is
60
+ what `eslint-base.js`'s `no-restricted-imports` rule for
61
+ `libs/*/service/**` enforces (wired at bootstrap) — a service reaching
62
+ a `*.adapter`, `drizzle-orm`, or `packages/db` fails lint.
63
+
64
+ A junction table (e.g. `order_items`) is one table, one module, with two
65
+ FK-by-ID columns instead of one, each resolved the same way.
66
+
67
+ Every module goes through the same shape, in order:
68
+
69
+ ```
70
+ schema (Drizzle) — types before data
71
+ contract (Zod / ts-rest) — the boundary
72
+ repository (port + Drizzle adapter)
73
+ service (domain logic) — imports only ports
74
+ controller (thin HTTP)
75
+ hook (TanStack Query) — Phase B only
76
+ ```
77
+
78
+ Plus, when an operation needs async **and the Queue add-on is on for this
79
+ project** (check `.hedgehog/addons.yaml`'s `queue.on`): **queue = port +
80
+ BullMQ adapter**, same port/adapter shape as the repository. The service
81
+ imports only ports. Queue is one-time project infra, not a compiled
82
+ layer — `full-stack-app/core.yaml` has no `queue` layer, so this step has
83
+ no `hedgehog verify` gate of its own; build it as part of the
84
+ `controller` layer's packet, verified by that layer's own check. If the
85
+ Queue add-on is off, there's no `apps/worker` and no queue step, full
86
+ stop — an operation that seems to want async processing on a Queue-off
87
+ project is a signal to revisit that add-on decision with `planner`, not
88
+ to build a one-off queue outside the add-on's scaffolding.
89
+
90
+ Every layer scaffolds from its own generator in `tools/generators/` (see
91
+ "Scaffolding a layer" below) — package shell, tags, files, barrel wiring,
92
+ and the layer's conventional shape all land in one deterministic step.
93
+ What's authored on top is the entity-specific delta: the field list and
94
+ its types, the module's business rules, and the UX intent behind its
95
+ screen.
96
+
97
+ ## Domain Module — Backend Steps (Phase A, every module in scope)
98
+
99
+ A horizontal pass across the whole backend — every module goes through
100
+ these before any module gets a hook or screen. Each row is one compiled
101
+ layer in `full-stack-app/core.yaml`; delegate each module's Phase A
102
+ layers to the `backend-eng` agent, one claimed packet per dispatch — it
103
+ builds the layer, `hedgehog verify` gates and commits it.
104
+
105
+ | # | Layer | Lives in | Commit |
106
+ |---|---|---|---|
107
+ | 1 | `schema` | `packages/db` (Drizzle) | `feat(<module>): schema` |
108
+ | 2 | `contract` | `packages/contracts` (Zod via `drizzle-zod` + ts-rest) | `feat(<module>): contract` |
109
+ | 3 | `repository` | `libs/<module>/repository` (port + Drizzle adapter) | `feat(<module>): repository` |
110
+ | 4 | `service` | `libs/<module>/service` (domain logic — imports only ports) | `feat(<module>): service` |
111
+ | 5 | `controller` | `apps/api` (thin HTTP, wires contract → service; bundles Queue infra, see above, if that add-on is on and this module needs it) | `feat(<module>): api` |
112
+
113
+ Repeat 1–5 per module in scope, via `hedgehog claim`/`hedgehog verify`.
114
+ The API is complete, typed, and callable (Postman/curl/contract tests)
115
+ before frontend work starts.
116
+
117
+ ## Domain Module — Frontend Steps (Phase B, after Phase A closes for the module)
118
+
119
+ | # | Layer | Lives in | Commit |
120
+ |---|---|---|---|
121
+ | 6 | `hook` | `packages/hooks` (TanStack Query) | `feat(<module>): hooks` |
122
+ | 6a | UX rationale | `docs/design/<module>.md`, `ux-planner` agent | bundled into layer 7's commit |
123
+ | 7 | `screen` | `apps/web`, plus `apps/mobile` when the Mobile add-on is on | `feat(<module>): screen-web` |
124
+
125
+ Phase B starts once Phase A is done for the scope. The frontend is a pure
126
+ consumer of an already-finished API. Delegate each module's Phase B
127
+ layers to the `front-end-eng` agent, same reasoning as `backend-eng` for
128
+ Phase A — one claimed packet per dispatch, in its own context. Step
129
+ 6a is where "how it should feel" gets decided — once per module, after
130
+ the `hook` layer's task is `complete` and before `front-end-eng` starts
131
+ the `screen` layer — via `ux-planner`, starting from whatever `planner`
132
+ filed in `docs/design/<module>-notes.md` at planning intake, or the raw
133
+ UX spec directly if that file is absent, or — where the archive holds
134
+ neither — from the contract and hook plus whatever the user supplies when
135
+ it asks. Its first run for a module also
136
+ signals to the user that Phase B has started, and is the point a mockup,
137
+ screenshot, or export (Google Stitch, Figma) can be handed over. It
138
+ writes `docs/design/<module>.md`, not its own compiled layer — the
139
+ `screen` layer's `hedgehog verify` is what gates and commits it.
140
+
141
+ ## The Loop (every unit of work)
142
+
143
+ 1. **Run `hedgehog claim --count N --owner <owner>`.** `<owner>` is this
144
+ session (a stable id — session id or equivalent). Claim is atomic and
145
+ lease-based, safe for concurrent claimers, and is the entry point into
146
+ the loop — `hedgehog next` still exists as a read-only preview of the
147
+ single next task, but claim is what actually reserves work. `--count
148
+ N` is a maximum, not a promise: it returns however many tasks are
149
+ safe to run together right now (the conflict predicate already
150
+ filtered them against each other), which may be fewer than N, or
151
+ zero. `hedgehog ready` previews the same decision without claiming
152
+ anything — CLAIMABLE vs HELD BACK, with the reason for each holdback —
153
+ useful for understanding the scheduler before committing to a claim.
154
+ 2. **Dispatch each claimed packet to its own subagent** — `backend-eng`
155
+ (Phase A) or `front-end-eng` (Phase B), matching each packet's ALLOWED
156
+ SCOPE — in ONE message with parallel tool calls, not one agent call
157
+ after another. This is a Claude session orchestrating via the Agent
158
+ tool's parallel-call mechanism: N claimed tasks means N Agent calls in
159
+ the same message.
160
+ 3. Each agent **runs typecheck/lint/test on its own work** (mirrors
161
+ lefthook, wired at bootstrap) as a sanity check before reporting
162
+ back — necessary, not sufficient. Per task, per agent: the agent
163
+ reports its work as done; it does not move the task and does not
164
+ commit.
165
+ 4. **As each report arrives, verify it — one at a time, serially.** Run
166
+ `hedgehog verify <task-id> --owner <owner>` (the same owner that
167
+ claimed it; verify requires the lease owner). Building happens in
168
+ parallel; verifying does not — verify writes a commit, and commits go
169
+ through one at a time. It checks the touched files against the
170
+ packet's ALLOWED SCOPE, runs the layer's VERIFICATION command, and on
171
+ a pass writes the commit (the exact Conventional Commit message from
172
+ the tables above, plus the updated build graph) and unlocks the next
173
+ layer. On a scope violation or a failing check, the task moves to
174
+ `blocked` with a `blocked_reason` of `scope_violation` or
175
+ `verification_failed`, and nothing downstream unlocks. Fix the work,
176
+ then run `hedgehog retry <task-id>` to return the task to `planned`,
177
+ claim it again (by task id — see below), and verify again —
178
+ `hedgehog verify` only accepts a task you currently hold in
179
+ `building`, so a blocked task has to go back through `retry` and
180
+ `claim` first. Don't hand-commit around it.
181
+
182
+ A `blocked` task anywhere in the graph — in this module or any other —
183
+ makes `hedgehog claim --count N` refuse to hand out anything at all,
184
+ with a non-zero exit naming the blocked task(s). `hedgehog status`
185
+ lists them too, under NEEDS ATTENTION. Fix and `retry` the named
186
+ task(s) before claiming more. A **targeted** `hedgehog claim <task-id>
187
+ --owner <owner>` is exempt — that's how the just-retried task gets
188
+ reclaimed in the step above. A lease the same `claim` call reaps for
189
+ having just expired is exempt too: that call still claims whatever
190
+ else is ready, and the reaped task lands in NEEDS ATTENTION for the
191
+ next `claim` call to stop on.
192
+ 5. **Repeat** — `hedgehog claim --count N --owner <owner>` again for the
193
+ next batch.
194
+
195
+ Each claimed packet is the full packet — STATUS/INTENT/RELEVANT
196
+ RULES/INHERITED DEBT/WHY NOW/BLOCKED DOWNSTREAM/ALLOWED
197
+ SCOPE/VERIFICATION — and its **INTENT** block carries the goal and
198
+ outcome of the whole intent, not just this layer's objective. A layer's
199
+ verify command runs the tests that layer wrote, so it measures internal
200
+ consistency, never coverage of what was asked; build the layer's share of
201
+ the goal and say so when the packet doesn't account for something the
202
+ goal asks for. When `hedgehog verify` closes the **last** layer of an
203
+ intent it prints the goal and outcome back as an **INTENT CHECK** — read
204
+ the built work against it there, because nothing else in the build does.
205
+
206
+ A layer that hits a limitation the next layer must compensate for
207
+ declares it with `hedgehog debt add <task-id> "<note>"`; the note lands
208
+ in the **INHERITED DEBT** section of every packet that depends on that
209
+ task. A comment in a source file is not a mechanism — nothing reads it.
210
+
211
+ Each `hedgehog verify` call commits exactly one layer, built right for
212
+ what's known now; a wrong layer is fixed forward later via the
213
+ Correction Protocol. Valid task statuses are `planned`, `ready`,
214
+ `building`, `verifying`, `complete`, and `blocked`; a task in `blocked`
215
+ also carries a `blocked_reason` (`scope_violation`, `verification_failed`,
216
+ or `lease_expired`).
217
+
218
+ ## Scaffolding a layer
219
+
220
+ `tools/generators/` holds one Nx generator per layer, and every layer
221
+ starts from its own:
222
+
223
+ ```bash
224
+ nx g ./tools/generators:schema --module=<module> --fields='<name:type,...>'
225
+ nx g ./tools/generators:contract --module=<module> --fields='<name:type,...>' [--toggleField=<boolField>]
226
+ nx g ./tools/generators:repository --module=<module>
227
+ nx g ./tools/generators:service --module=<module> [--toggleField=<boolField>]
228
+ nx g ./tools/generators:controller --module=<module> --fields='<name:type,...>' [--toggleField=<boolField>]
229
+ nx g ./tools/generators:hook --module=<module> [--toggleField=<boolField>]
230
+ nx g ./tools/generators:screen --module=<module>
231
+ ```
232
+
233
+ `--module` is the domain module's plural kebab-case name (`tasks`,
234
+ `order-items`). `--fields` is a comma-separated list of `name:type` pairs
235
+ over `string`, `text`, `boolean`, `integer`, and `timestamp`, with a
236
+ trailing `?` marking the column nullable
237
+ (`--fields='title:string,done:boolean,dueDate:timestamp?'`); `contract`
238
+ and `controller` take the same list the module's `schema` was generated
239
+ with. A `string` field takes an optional length in parentheses
240
+ (`title:string(500)`), threaded to both the Drizzle `varchar` and the Zod
241
+ `.max()` so the two cannot disagree; omitted, it is 255. Check every
242
+ length against the intent's own rules in the packet's **RELEVANT RULES** —
243
+ a rule like "at most 500 characters" is the field list's business, not
244
+ the authored delta's, and a Zod bound that outruns its column surfaces as
245
+ a driver error at the database rather than a 400 at the boundary.
246
+
247
+ `--toggleField` names a boolean field from the schema to expose as a
248
+ toggle, and is passed to `contract`, `service`, `controller`, and `hook`
249
+ alike — one flag, four layers, so the route, the domain method, the
250
+ handler, and the mutation are generated from one source. The toggle is
251
+ server-side by construction: `POST /<module>/:id/toggle` carries no body,
252
+ and the service reads the current value and flips it inside the same
253
+ transaction its `update` uses. The client sends only the id, so a stale
254
+ cached row cannot overwrite a newer state — which is exactly what
255
+ computing the new value on the client would do, losing one of any two
256
+ flips that raced. Note that `@ts-rest/core` generates no `body` parameter
257
+ for a `c.noBody()` route: the call is `client.toggle({ params: { id } })`,
258
+ and passing `body: undefined` is a compile error.
259
+
260
+ Each generator lands the whole conventional shape of its layer in one
261
+ deterministic step — the package shell (`package.json`, `tsconfig*.json`,
262
+ `vitest.config.mts`, `src/index.ts`) where the layer creates one, the
263
+ `nx.tags` pair `packages/config/eslint-base.js`'s `depConstraints` keys
264
+ on, the port-discipline file suffixes lint checks for, the Nest module
265
+ and controller pair with `@Controller()` left bare (the ts-rest contract
266
+ already encodes full route paths), and every barrel export the new files
267
+ need. Hand-copying a sibling module's files invites exactly the drift
268
+ `hedgehog verify`'s lint step then has to catch: missing tags, missing
269
+ project references, a doubled route prefix.
270
+
271
+ What the generator lands is the layer's skeleton, not the layer. Author
272
+ the entity-specific delta on top: the module's business rules in
273
+ `service`, its domain-error mapping in `controller`, and — for `screen`,
274
+ which is skeleton-only by design — the layout, information hierarchy, and
275
+ interaction pattern from `ux-planner`'s rationale, over the placeholders
276
+ the generator leaves for the list, filter shell, empty state, and form.
277
+
278
+ Registration inside `apps/api` is automatic and stays that way:
279
+ `apps/api/src/app/feature-modules.ts` globs
280
+ `apps/api/src/app/*/*.module.ts` and is regenerated by the
281
+ `generate-feature-modules` Nx target that `build`/`typecheck`/`test`
282
+ depend on. Never register a module by editing `app.module.ts` — a shared
283
+ file no module-scoped task can safely touch. Validation is ts-rest + Zod,
284
+ so this core has no Nest DTOs and no class-validator.
285
+
286
+ The root page is the same: `apps/web/src/app/page.tsx` renders whatever
287
+ `module-routes.ts` holds, and that file is regenerated by the
288
+ `generate-module-routes` Nx target from every
289
+ `apps/web/src/app/*/page.tsx` on disk. A screen layer creates its own
290
+ `page.tsx` in its own directory and the root page picks it up — never
291
+ edit either the root page or the generated file to add a link.
292
+
293
+ **A new package needs wiring into the workspace before `hedgehog verify`
294
+ runs on it.** A package that exists on disk isn't yet part of the
295
+ workspace:
296
+
297
+ ```bash
298
+ pnpm install # link the new workspace:* deps
299
+ pnpm nx sync # regenerate TypeScript project references
300
+ ```
301
+
302
+ `pnpm-workspace.yaml` already globs `packages/*`, `apps/*` and `libs/*/*`,
303
+ so a package under any of those needs no edit there. This is the common
304
+ case, not an edge case: any layer that is the first arrival in a package
305
+ (`contract`, `hook`, each module's `repository` and `service`) or that
306
+ wires a new package into an existing one (`controller`, adding the
307
+ module's `contracts`/`repository`/`service` packages to `apps/api`) needs
308
+ it — on a module's first pass through Phase A/B that is most of the
309
+ layers, not an occasional one.
310
+
311
+ The building agent runs `pnpm install` / `pnpm nx sync` and reports back
312
+ which shared files changed (typically `pnpm-lock.yaml`, root
313
+ `tsconfig.json`, and — on a `controller` layer — `apps/api/package.json`,
314
+ `apps/api/tsconfig.app.json`), because it has the shell access to run
315
+ them, but it never commits: no agent reporting success moves a task or
316
+ touches git, only `hedgehog verify`'s passing exit code does (see the
317
+ building agents' own Workflow step on this). Committing those shared
318
+ files is the orchestrating session's job, done between dispatch and
319
+ `hedgehog verify` on every layer where the agent flagged a change: expect
320
+ it, don't wait to be reminded.
321
+
322
+ ```bash
323
+ git add pnpm-lock.yaml tsconfig.json # plus apps/*/package.json,
324
+ # apps/*/tsconfig.app.json on a
325
+ # controller layer
326
+ git commit -m "chore(workspace): sync project references"
327
+ ```
328
+
329
+ These files are mechanically derived by `pnpm install` and `pnpm nx
330
+ sync`, not authored content, and sit outside every module-scoped layer's
331
+ scope — they belong to no layer, and no override covers them. Committing
332
+ them separately, before `hedgehog verify` runs, keeps the layer's own
333
+ commit exactly the layer.
334
+
335
+ This is the orchestrating session's step rather than a verify post-step
336
+ on purpose: `hedgehog verify` gates the tree it's handed, and a gate that
337
+ mutates that tree would manufacture the scope violation it then reports.
338
+
339
+ ## First arrival in a package
340
+
341
+ Every layer scope names a directory *inside* a package
342
+ (`packages/contracts/src/{module}/**`, `libs/{module}/repository/**`), and
343
+ on the first module through that layer the package itself doesn't exist
344
+ yet. Its shell — `package.json`, `tsconfig*.json`, `vitest.config.mts`,
345
+ `src/index.ts` — necessarily lands outside the layer's scope glob, because
346
+ no `{module}`-bearing glob can cover a package root. Left alone those files
347
+ sit on disk uncommitted until the `join` layer's `**` scope sweeps them in,
348
+ so `git log -- packages/contracts/` shows source with no buildable package
349
+ behind it for the whole middle of the build.
350
+
351
+ A generator can also drop shared, package-wide source at the `src/` root
352
+ alongside the module's own files on that same first pass — the `contract`
353
+ generator's `timestamp.ts` is one (a shared Zod util every module in the
354
+ package imports, written once, sibling to `src/index.ts`). That file needs
355
+ the same widening as the shell itself.
356
+
357
+ The packet says so: when the package a scope points into has no
358
+ `package.json` on disk yet, `hedgehog next`/`show` prints a **FIRST
359
+ ARRIVAL** section under ALLOWED SCOPE carrying the exact command for that
360
+ task. Run it before building — the widening is only available while the
361
+ task is still `ready`, and a verify that rejects the shell paths blocks
362
+ the task and turns this into a five-command recovery.
363
+
364
+ ```bash
365
+ hedgehog override add TASKS-CONTRACT \
366
+ --scope 'packages/contracts/*' \
367
+ --scope 'packages/contracts/src/*' \
368
+ --reason 'first module through the contract layer also creates the package shell'
369
+ ```
370
+
371
+ `packages/contracts/src/*` is non-recursive, so it covers `src/index.ts`
372
+ and `src/timestamp.ts` without also granting the module subdirectory the
373
+ layer's own `packages/contracts/src/{module}/**` scope already covers.
374
+
375
+ `.hedgehog/overrides/*.json` is additive, per-task, committed, and replayed
376
+ by `plan`, `--recompile` and `db rebuild` alike, so the exception survives a
377
+ rebuild and stays reviewable in the diff — unlike a hand-edited task row,
378
+ which the next rebuild silently drops. It widens exactly the one task that
379
+ creates the package, not the layer, so module two's task keeps the narrow
380
+ scope.
381
+
382
+ Which tasks need it: the first module through `contract`
383
+ (`packages/contracts`) and through `hook` (`packages/hooks`), and every
384
+ module's `repository` and `service`, since `libs/{module}/repository` and
385
+ `libs/{module}/service` are new libs per module — there, the layer's own
386
+ `libs/{module}/repository/**` glob already covers the package root, so no
387
+ override is needed. `packages/db` and `packages/config` ship with core, so
388
+ `schema` never needs one. `controller` never needs one either — `apps/api`
389
+ ships with core, and its `apps/api/src/app/{module}/**` scope already
390
+ covers the module's generated directory.
391
+
392
+ Never widen a scope to route around a violation the Correction Protocol
393
+ should handle — this is for a package shell the layer genuinely creates,
394
+ nothing else. The shell itself comes from the layer's generator
395
+ ("Scaffolding a layer" above), which is also where the workspace wiring a
396
+ new package needs lives.
397
+
398
+ ## Intra-step conventions
399
+
400
+ The Nx boundaries, phase gate, and lint own the *structural* rules
401
+ (what imports what, what gets built when). These are the conventions
402
+ *inside* a step that those gates can't see — apply them uniformly so a
403
+ fresh-context session builds module N the same way it built module 1. The
404
+ `reviewer` agent checks these at a phase boundary.
405
+
406
+ - **Errors are thrown, typed, and domain-named.** A service throws a
407
+ domain error (`OrderNotFoundError`, not a bare `Error` or an HTTP
408
+ exception) — services don't know they're behind HTTP. The controller is
409
+ the only layer that maps domain errors to status codes. Never return
410
+ `null`/`undefined` to signal a failure a caller must branch on.
411
+ - **Repository not-found returns `undefined`; the service decides.** A
412
+ `findById` that misses returns `undefined` (a plain absence, not an
413
+ error); the service turns that into a thrown domain error when the
414
+ operation requires the row. Adapters don't throw domain errors — they
415
+ report absence, the service interprets it.
416
+ - **Validation lives at the contract boundary, once.** Input is
417
+ Zod-validated at the controller via the ts-rest contract. Past that
418
+ boundary, types are trusted — services and repositories don't re-parse.
419
+ A service-level invariant that isn't expressible in the Zod schema
420
+ (e.g. "can't cancel after payment") is enforced in the service as a
421
+ thrown domain error, not a second validation pass.
422
+ - **Multi-write operations are transactional.** A service method that
423
+ writes more than once wraps the writes in one Drizzle transaction,
424
+ passed through the port — partial writes never escape a failed
425
+ operation.
426
+ - **Services are pure domain logic.** No logging, no HTTP, no queue
427
+ mechanics inside a service method — those live at the controller /
428
+ adapter edge. A service reads as the business rule and nothing else.
429
+
430
+ ## Friction log
431
+
432
+ Real friction during a build — an agent's instructions were unclear, a
433
+ redline had to be issued twice for the same underlying gap, the user
434
+ had to correct the same kind of mistake more than once, or user
435
+ feedback implied something was wrong even without a direct correction
436
+ (a preference stated once that, read plainly, means an earlier step
437
+ missed something) — is signal worth keeping past this session, separate
438
+ from the Correction Protocol that fixes it in the moment. Log one entry
439
+ via `hedgehog friction add "<note>" [--task <task-id>]` when that
440
+ happens: what was tried, what went wrong or was implied, why if visible,
441
+ and the commit/message it traces to, all in the note text; pass `--task`
442
+ with the layer's task id when the friction traces to one. This is a log,
443
+ not a todo list — don't let it block or slow the Loop; log and keep
444
+ moving. `tweaker` reads it (via `hedgehog friction list`) once the build
445
+ reaches its Stop Condition.
446
+
447
+ ## Correction Protocol
448
+
449
+ When a downstream step reveals an upstream step was wrong:
450
+
451
+ 1. **Quiesce.** Dispatch nothing new. Let in-flight tasks finish and
452
+ verify normally — do NOT kill running subagents. Release anything
453
+ claimed but not yet started (`hedgehog release <task-id> --owner
454
+ <owner>`).
455
+ 2. Once nothing is in flight (`hedgehog quiesce` exits 0), patch the
456
+ upstream step directly, in place.
457
+ 3. Fast-forward every dependent step that breaks, each its own small
458
+ commit. If the patched step lives in a workspace package (e.g.
459
+ `packages/hooks`, `packages/contracts`) that a running `web`/`mobile`
460
+ dev server consumes, run that package's `nx run <pkg>:build` before
461
+ re-verifying — the dev server resolves the package's built `dist/`,
462
+ not its `src/`, so an unbuilt patch looks unchanged to anything
463
+ downstream even though the source is fixed.
464
+ 4. The commit messages are the explanation.
465
+ 5. Resume — `hedgehog claim` again.
466
+
467
+ Quiescing is correct, not a cautious fallback. The conflict predicate
468
+ already guarantees a correction cannot collide with in-flight work: if
469
+ the correction's scope conflicted with something currently building, the
470
+ scheduler would not have co-scheduled it in the first place. Letting
471
+ in-flight tasks finish and verify rather than killing them costs nothing
472
+ and throws away no progress.
473
+
474
+ The orchestrating session runs this protocol. A phase-owning agent that
475
+ hits the problem reports it rather than correcting across steps: the
476
+ commits at step 3 are the session's act, the same way `hedgehog verify`
477
+ always is.
478
+
479
+ Use `conventional-commits` when a correction touches several steps in one
480
+ working-tree pass and needs splitting back into per-step commits.
481
+
482
+ ### Post-build entry
483
+
484
+ The protocol also runs after a build has reached its Stop Condition, when
485
+ a `tweaker` session finds that something structural is wrong rather than
486
+ something small (`tweaker` routes it here). Steps 2, 3, and 4 are
487
+ unchanged. The two ends differ:
488
+
489
+ - There is nothing to **quiesce** — no task is in flight. Start by naming
490
+ which committed step was wrong and what revealed it.
491
+ - There is no loop to **resume**: every task is already `complete`, so
492
+ `hedgehog claim` has nothing to claim. Return to the `tweaker` session
493
+ instead.
494
+
495
+ Every task the correction touches is already `complete` and stays that
496
+ way — a correction is fixed forward in new commits, never by reopening a
497
+ finished task. Verify each patched step by running that step's own verify
498
+ command directly. Log the correction with `hedgehog friction add` so the
499
+ next friction review sees what the build got wrong.
500
+
501
+ ## Phase Transition Checks
502
+
503
+ Before starting Phase B for a module, confirm:
504
+
505
+ - `hedgehog status` shows that module's `controller` task `complete`
506
+ (equivalently, a `feat(<module>): api` commit exists).
507
+ - The contract is callable and typed (contract tests pass).
508
+
509
+ Use the `reviewer` agent for this — it checks what the mechanical gate
510
+ can't. Everything lefthook already enforces (typecheck, lint, unit test
511
+ pass/fail) is out of scope for that review; these are the checks this
512
+ core adds on top of it, and the list `reviewer` works from at a
513
+ full-stack-app phase boundary:
514
+
515
+ - **Port discipline**: a module's port interface and its Drizzle adapter
516
+ share one lib, so the tag graph has to allow `type:service →
517
+ type:adapter` and the real check is at the import level: does the
518
+ service import the port from the repository lib's entry point, or the
519
+ concrete `*.adapter`? Does anything in `apps/api` outside a
520
+ `*.module.ts` construct an adapter? `eslint-base.js`'s
521
+ `no-restricted-imports` rules catch the named cases — read the actual
522
+ imports anyway, since an adapter file not named `*.adapter.ts` opts
523
+ itself out of the rule. Use `nx show project <name> --json` (per the
524
+ `nx-workspace` skill) to check a project's resolved tags and
525
+ dependencies rather than reading `project.json` directly — it only
526
+ holds partial configuration, not tags inferred by plugins.
527
+ - **FK-by-ID discipline**: does a module's repository/service reach into
528
+ another module's tables directly, or only resolve related entities by
529
+ ID at the contract/controller layer (cross-module references, above)?
530
+ - **Module granularity**: is this actually one table = one module, or has
531
+ scope crept — two tables sharing a service, or a junction table
532
+ absorbed into one side's module instead of standing alone?
533
+ - **Contract shape**: does the Zod/ts-rest contract match what Phase B
534
+ will need, or does it leak implementation detail that will force a
535
+ breaking change once hooks are built against it?
536
+ - **Phase leakage**: any hook or screen code, or frontend-shaped
537
+ reasoning, showing up before this module has a `feat(<module>): api`
538
+ commit?
539
+ - **Queue seam**: if the Queue add-on is on and queue infra was added,
540
+ does the operation genuinely need async (long-running, retries,
541
+ fan-out) — or was the seam reached for out of habit? If the Queue
542
+ add-on is off (check `.hedgehog/addons.yaml`'s `queue.on`), there should
543
+ be no `apps/worker` and no queue infra at all for this module — queue
544
+ infra appearing anyway is itself a finding, not something to review the
545
+ contents of.
546
+ - **Intra-step conventions**: does the module follow the conventions the
547
+ gate can't see (Intra-step conventions, above)? Check against that list
548
+ rather than re-deriving it. A module drifting from them is a Warning
549
+ unless it breaks Phase B.
550
+ - **Security/correctness**: unvalidated input reaching a Drizzle query
551
+ outside the Zod-validated contract boundary, secrets, obvious logic
552
+ errors — same bar any reviewer would apply, scoped to what's new since
553
+ the last review point.
554
+
555
+ The review point is the last `feat(<module>): api` commit; `git diff`
556
+ from there, then read every layer of the module rather than the diff
557
+ alone — boundary violations are invisible from a diff.
558
+
559
+ Before starting Phase A for a module, confirm it's inside the stated scope
560
+ boundary from planning intake (`planner`). If not, stop and ask — and if
561
+ the answer is that the scope really should grow, that's `planner`'s
562
+ Re-entry pass, which adds it to the graph properly. Don't build a module
563
+ the graph doesn't have a task for.
564
+
565
+ ## Rules
566
+
567
+ - **Phase A closes before Phase B opens.** Every module in scope has a
568
+ working, tested API before any hook or screen starts.
569
+ - **Concurrent within a phase, bounded by the scheduler.** Never assume
570
+ two tasks are safe to run together because they look independent — ask
571
+ `hedgehog ready`.
572
+ - **Queue infra is conditional twice over** — only if the Queue add-on is
573
+ on for this project at all (per `.hedgehog/addons.yaml`'s `queue.on`),
574
+ and even then only when a given operation genuinely needs async
575
+ (long-running, retries, fan-out); the normal case has no queue.
576
+ - **A wrong step gets fixed at its source** — the Correction Protocol, not
577
+ a downstream workaround.
578
+ - **Tests gate every commit** in the sequence.
579
+ - A module's frontend code (hook, screen) is built after its API is
580
+ committed.
581
+ - The screen step doesn't start blank — `ux-planner` runs once per module,
582
+ after the hook is committed, before `front-end-eng` starts the screen.
583
+ - `packages/config` is the single source for shared config; a per-app
584
+ override request signals to fix the base config at the source.
585
+
586
+ ## Stop Condition
587
+
588
+ A build session ends when `hedgehog status` shows every task for every
589
+ module in scope `complete` (Phase A and Phase B both closed), or when
590
+ scope is ambiguous enough that continuing means guessing — ask one
591
+ question and wait.
592
+
593
+ On the former (a real build completion, not an ambiguity stop), offer a
594
+ fresh-context handoff before doing anything else: tell the user the
595
+ build is complete, and that clearing context now costs nothing. The
596
+ permanent record is the committed intents, friction log, root
597
+ `core.yaml` (the shipped core definition — not `.hedgehog/core.yaml`,
598
+ which only exists on an authored core), and the commit history itself —
599
+ not `.hedgehog/hedgehog.db`, which is
600
+ gitignored and derived, rebuildable at any time via `hedgehog db
601
+ rebuild`. That's what makes the next session cheap.
602
+
603
+ Before offering that handoff, run `hedgehog boundary` and only declare
604
+ the Stop Condition met once it exits 0. Every task showing `complete` is
605
+ not sufficient on its own: a lease can be outstanding without a visible
606
+ status change, and the working tree can still hold uncommitted work.
607
+ `boundary` checks all three — nothing in flight, clean tree, last closed
608
+ task completed its intent — and names which one failed when it exits
609
+ non-zero. `hedgehog quiesce` covers only the first of the three; it is
610
+ the right check mid-correction, not the right check for a handoff.
611
+
612
+ The same command answers the mid-build question the project instructions
613
+ file's **Managing context** section depends on: whether *this* moment,
614
+ not just the end of the build, is one to clear the conversation at. Run
615
+ it at any point you're considering `/clear`, and start the next session
616
+ from `hedgehog boundary --handoff`, which prints where the build is,
617
+ what's next and why, and what's blocked, straight from the graph.
618
+
619
+ Name **both** ways forward, because which one applies depends on what the
620
+ user wants next:
621
+
622
+ - **Adjustments to what's built** — a `tweaker` session, in a *new* chat
623
+ window, not a subagent call inside this one — this session's context
624
+ has been building the whole project and is exactly what "clearing
625
+ context now costs nothing" above means to discard. Tell the user
626
+ plainly: close this chat window and open a new one, then paste this to
627
+ start it:
628
+
629
+ > The build is complete. Use the tweaker agent: first review the
630
+ > friction log and ask me for feedback on the build, then take my
631
+ > tweak requests one at a time.
632
+
633
+ In the new window, `tweaker` starts clean, reviews the friction log
634
+ (`hedgehog friction list`) once for a possible discipline-improvement
635
+ suggestion, and takes tweak requests one at a time from there.
636
+ - **New scope** — a new module, a new feature, anything beyond adjusting
637
+ what exists — goes to `planner`, which runs
638
+ `hedgehog-planning-intake`'s Re-entry pass: it adds intents for the new
639
+ work without re-running planning from scratch, and without disturbing
640
+ anything already built. A completed build is extendable, not sealed.
641
+
642
+ Don't start making tweaks or planning new scope in the current,
643
+ already-large context; that's what the fresh session is for.