@pikku/skills 0.12.21 → 0.12.25

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 (101) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +1 -1
  4. package/skills/pikku-addon/SKILL.md +10 -9
  5. package/skills/pikku-agent/SKILL.md +67 -316
  6. package/skills/pikku-agent/references/agents.md +299 -0
  7. package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
  8. package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
  9. package/skills/pikku-architect/SKILL.md +264 -0
  10. package/skills/pikku-auth/SKILL.md +89 -0
  11. package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +42 -47
  12. package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
  13. package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
  14. package/skills/{pikku-permissions/SKILL.md → pikku-auth/references/permissions.md} +5 -24
  15. package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +3 -23
  16. package/skills/pikku-build/SKILL.md +87 -0
  17. package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +77 -25
  18. package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
  19. package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +1 -1
  20. package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
  21. package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
  22. package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +6 -22
  23. package/skills/{pikku-build-app → pikku-build}/references/ship.md +7 -1
  24. package/skills/pikku-concepts/SKILL.md +75 -8
  25. package/skills/pikku-concepts/references/concept-mapping.md +8 -8
  26. package/skills/pikku-deploy/SKILL.md +158 -0
  27. package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
  28. package/skills/pikku-deploy/references/cloudflare.md +104 -0
  29. package/skills/pikku-deploy/references/express.md +92 -0
  30. package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
  31. package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
  32. package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
  33. package/skills/pikku-deploy/references/uws.md +72 -0
  34. package/skills/pikku-deploy/references/ws.md +75 -0
  35. package/skills/pikku-emails/SKILL.md +3 -2
  36. package/skills/pikku-fabric/SKILL.md +20 -10
  37. package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
  38. package/skills/pikku-i18n/SKILL.md +60 -207
  39. package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
  40. package/skills/pikku-i18n/references/messages.md +218 -0
  41. package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
  42. package/skills/pikku-knowledge/SKILL.md +14 -0
  43. package/skills/pikku-kysely/SKILL.md +13 -13
  44. package/skills/pikku-meta/SKILL.md +58 -130
  45. package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
  46. package/skills/pikku-meta/references/meta.md +114 -0
  47. package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
  48. package/skills/pikku-middleware/SKILL.md +8 -8
  49. package/skills/pikku-n8n-import/SKILL.md +0 -1
  50. package/skills/pikku-react/SKILL.md +50 -298
  51. package/skills/pikku-react/references/client.md +293 -0
  52. package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
  53. package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
  54. package/skills/pikku-scenario/SKILL.md +64 -49
  55. package/skills/pikku-scenario/references/persona-run.md +148 -0
  56. package/skills/pikku-service-backends/SKILL.md +154 -0
  57. package/skills/pikku-service-backends/references/aws.md +106 -0
  58. package/skills/pikku-service-backends/references/backblaze.md +57 -0
  59. package/skills/pikku-service-backends/references/mongodb.md +90 -0
  60. package/skills/pikku-service-backends/references/redis.md +75 -0
  61. package/skills/pikku-service-backends/references/schema.md +63 -0
  62. package/skills/pikku-services/SKILL.md +68 -291
  63. package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
  64. package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
  65. package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
  66. package/skills/pikku-services/references/services.md +272 -0
  67. package/skills/pikku-software-archaeology/README.md +5 -1
  68. package/skills/pikku-software-archaeology/SKILL.md +15 -2
  69. package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
  70. package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
  71. package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
  72. package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
  73. package/skills/pikku-webhook/SKILL.md +199 -0
  74. package/skills/pikku-wiring/SKILL.md +180 -0
  75. package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
  76. package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
  77. package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
  78. package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +4 -40
  79. package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
  80. package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
  81. package/skills/{pikku-realtime/SKILL.md → pikku-wiring/references/realtime.md} +2 -25
  82. package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
  83. package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
  84. package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
  85. package/skills/pikku-workflow/SKILL.md +3 -3
  86. package/skills/pikku-aws/SKILL.md +0 -161
  87. package/skills/pikku-backblaze/SKILL.md +0 -104
  88. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
  89. package/skills/pikku-deploy-express/SKILL.md +0 -122
  90. package/skills/pikku-deploy-uws/SKILL.md +0 -144
  91. package/skills/pikku-mongodb/SKILL.md +0 -113
  92. package/skills/pikku-product-second-opinion/README.md +0 -43
  93. package/skills/pikku-redis/SKILL.md +0 -99
  94. package/skills/pikku-schema-ajv/SKILL.md +0 -83
  95. package/skills/pikku-schema-cfworker/SKILL.md +0 -82
  96. package/skills/pikku-ws/SKILL.md +0 -87
  97. /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
  98. /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
  99. /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
  100. /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
  101. /package/skills/{pikku-realtime/references/other-routes.md → pikku-wiring/references/realtime-other-routes.md} +0 -0
@@ -0,0 +1,272 @@
1
+ # Pikku Services (Dependency Injection)
2
+
3
+
4
+ ## Before You Start
5
+
6
+ ```bash
7
+ pikku info functions --verbose # See which services existing functions use
8
+ pikku info tags --verbose # Understand project organization
9
+ ```
10
+
11
+ ## API Reference
12
+
13
+ ### `pikkuServices(factory)` — singleton services (created once at startup)
14
+
15
+ ```typescript
16
+ import { pikkuServices } from '#pikku/setup'
17
+ import { ConsoleLogger } from '@pikku/core/services'
18
+ import { JoseJWTService } from '@pikku/jose'
19
+
20
+ export const createSingletonServices = pikkuServices(
21
+ async (config, existingServices?) => {
22
+ // config: your CoreConfig object
23
+ // existingServices: optional, for chaining factories
24
+ const logger = new ConsoleLogger()
25
+ const database = new DatabasePool(config.database)
26
+ await database.connect()
27
+ const jwt = new JoseJWTService(
28
+ async () => [{ id: 'my-key', value: config.jwtSecret }],
29
+ logger
30
+ )
31
+ return { config, logger, database, jwt, books: new BookService() }
32
+ }
33
+ )
34
+ ```
35
+
36
+ ### `pikkuWireServices(factory)` — per-request services (fresh per HTTP request, queue job, CLI command, etc.)
37
+
38
+ ```typescript
39
+ import { pikkuWireServices } from '#pikku/setup'
40
+
41
+ export const createWireServices = pikkuWireServices(
42
+ async (singletonServices, wire) => {
43
+ // singletonServices: all singleton services
44
+ // wire: transport context (session, channel, etc.)
45
+ // Pikku merges these with singleton services automatically
46
+ return {
47
+ userSession: createUserSessionService(wire),
48
+ dbTransaction: new DatabaseTransaction(singletonServices.database),
49
+ }
50
+ }
51
+ )
52
+ ```
53
+
54
+ ### `pikkuServerLifecycle(hooks)` — startup and shutdown work
55
+
56
+ A service factory should **construct** services, not run startup side effects. Seeding a database, warming a cache, starting a background consumer or draining a queue belongs in lifecycle hooks, which receive the singleton services after they are built:
57
+
58
+ ```typescript
59
+ // src/lifecycle.ts
60
+ import { pikkuServerLifecycle } from '@pikku/core'
61
+ import type { SingletonServices } from '../types/application-types.js'
62
+
63
+ export const lifecycle = pikkuServerLifecycle<SingletonServices>({
64
+ beforeStart: async ({ kysely }) => {
65
+ await runMigrations(kysely) // before the port opens
66
+ },
67
+ afterStart: async (services) => {
68
+ await seedDevData(services) // server is accepting traffic
69
+ },
70
+ beforeStop: async ({ queueService }) => {
71
+ await queueService.drain() // services are still alive here
72
+ },
73
+ afterStop: async () => {
74
+ await releaseExternalLock() // services are ALREADY stopped
75
+ },
76
+ })
77
+ ```
78
+
79
+ Every hook is optional. Order is `beforeStart` → server starts → `afterStart`, then on SIGINT `beforeStop` → services stopped → server stopped → `afterStop`.
80
+
81
+ **`afterStop` runs after the singleton services have been stopped.** It still receives the services object, but the services inside it are shut down — using one there is a use-after-close bug. Anything that needs a live service goes in `beforeStop`.
82
+
83
+ Export **exactly one** `pikkuServerLifecycle` from anywhere in `srcDirectories`; the inspector finds it by the wrapper call, so the filename is free (`src/lifecycle.ts` by convention). It must be an exported `const` initialized with a direct call to `pikkuServerLifecycle` — a re-export or a conditional wrapper is invisible to the inspector.
84
+
85
+ **Only `pikku dev` and `pikku serve` run these hooks.** If you bootstrap your own server (Express, Fastify, uWS, Lambda, Cloudflare, Next.js), no runtime adapter invokes them — put the work in your entrypoint instead.
86
+
87
+ ### Auto-Generated Service Manifest
88
+
89
+ After `npx pikku all`, Pikku generates `.pikku/pikku-services.gen.ts`, a manifest of which services are actually used by wired functions:
90
+
91
+ ```typescript
92
+ export const requiredSingletonServices = {
93
+ database: true, // used by getUser, deleteUser
94
+ audit: true, // used by deleteUser
95
+ cache: false, // not used by any wired function
96
+ jwt: true, // used by auth middleware
97
+ } as const
98
+
99
+ export type RequiredSingletonServices = Pick<
100
+ SingletonServices,
101
+ 'database' | 'audit' | 'jwt'
102
+ > &
103
+ Partial<Omit<SingletonServices, 'database' | 'audit' | 'jwt'>>
104
+ ```
105
+
106
+ ## Usage Patterns
107
+
108
+ ### Using Services in Functions
109
+
110
+ **Every service must be declared in `SingletonServices` (or `Services`) in `application-types.d.ts`.** Never access a service via a body-level cast (`services as typeof services & { myService: MyService }`) — that means the type is missing. Add the import and the field to `SingletonServices`, then destructure inline in the function signature. The inspector emits `SERVICES_NOT_DESTRUCTURED` (`PKU410`) and tree-shaking breaks when the first param is a plain identifier rather than an object pattern. Never `new` a service inside a function — services arrive only via injection.
111
+
112
+ ```typescript
113
+ // ✅ Correct — inline destructure, no cast
114
+ const getUser = pikkuFunc({
115
+ title: 'Get User',
116
+ func: async ({ db, logger, jwt }, { userId }) => {
117
+ logger.info('Fetching user', { userId })
118
+ return { user: await db.getUser(userId) }
119
+ },
120
+ })
121
+
122
+ // ❌ Wrong — named param + body cast; inspector warns + tree-shaking breaks
123
+ const getUser = pikkuFunc({
124
+ func: async (services, { userId }) => {
125
+ const { db } = services as typeof services & { db: DbService }
126
+ // ...
127
+ },
128
+ })
129
+ ```
130
+
131
+ ### Services Are Never Optional Inside a Function
132
+
133
+ **Never write a `if (!service) throw ...` existence guard in a function body.** It is dead code, and it defeats the platform.
134
+
135
+ Optionality lives in exactly one place — `services.ts` / the `SingletonServices` declaration — and it means _"this may not be created"_, not _"this may be missing at call time"_. A service is optional precisely because **nothing destructures it**, and the generated `requiredSingletonServices` manifest therefore never marks it for creation. The moment any wired function destructures it, Pikku creates it and guarantees it is there.
136
+
137
+ The types enforce this rather than merely documenting it. The inspector records the services destructured by every wired `func`, `permissions` **and** `middleware`, and emits them as `RequiredSingletonServices`. The generated function types then default their service parameter to:
138
+
139
+ ```typescript
140
+ export type WiredSingletonServices = RequiredSingletonServices &
141
+ SingletonServices
142
+ export type WiredServices = SecretlessServices<
143
+ RequiredSingletonServices & Services
144
+ >
145
+ ```
146
+
147
+ The `SecretlessServices<...>` wrapper is why `secrets` never appears in a
148
+ function's services: it is stripped at the type level, not merely omitted by
149
+ convention. Read secrets in a service factory or middleware and hand the value
150
+ to a service instead.
151
+
152
+ so a service that is `foo?: Foo` in `SingletonServices` arrives as a non-optional `Foo` in every function, permission and middleware that uses it. There is nothing to guard against.
153
+
154
+ ```typescript
155
+ // ✅ Correct — destructure and use; creation is guaranteed by the manifest
156
+ const listThreads = pikkuFunc({
157
+ func: async ({ agentRunService }, { threadId }) => {
158
+ return await agentRunService.getThreadMessages(threadId)
159
+ },
160
+ })
161
+
162
+ // ❌ Wrong — unreachable guard; signals a misunderstanding of service wiring
163
+ const listThreads = pikkuFunc({
164
+ func: async ({ agentRunService }, { threadId }) => {
165
+ if (!agentRunService) throw new MissingServiceError('agentRunService')
166
+ return await agentRunService.getThreadMessages(threadId)
167
+ },
168
+ })
169
+ ```
170
+
171
+ If a service really is conditional at runtime (e.g. an optional integration a deployment may not configure), that is a **configuration** concern: branch on config, or fail fast at startup in `services.ts` — not per-request in every function.
172
+
173
+ ### Dynamic Import Optimization
174
+
175
+ Use the generated manifest to conditionally import heavy dependencies — only the services actually wired get instantiated:
176
+
177
+ ```typescript
178
+ import { requiredSingletonServices } from '.pikku/pikku-services.gen.js'
179
+
180
+ const createSingletonServices = pikkuServices(async (config) => {
181
+ const logger = new ConsoleLogger()
182
+
183
+ let jwt: JWTService | undefined
184
+ if (requiredSingletonServices.jwt) {
185
+ const { JoseJWTService } = await import('@pikku/jose')
186
+ jwt = new JoseJWTService(keys, logger)
187
+ }
188
+
189
+ let database: Database | undefined
190
+ if (requiredSingletonServices.database) {
191
+ database = await createDatabase(config.databaseUrl)
192
+ }
193
+
194
+ return { config, logger, jwt, database }
195
+ })
196
+ ```
197
+
198
+ ### Audit Wire Service
199
+
200
+ `createInvocationAudit` + `createAuditedKysely` add per-request audit buffering that flushes on request close (no-op if `audit` is unconfigured). For the full pattern, no-op behavior, custom-event usage, and Fabric notes, read `references/audit-wire-service.md`.
201
+
202
+ ### Built-in Services
203
+
204
+ | Service | Package | Purpose |
205
+ | ----------------------- | ---------------------- | --------------------------------------- |
206
+ | `ConsoleLogger` | `@pikku/core/services` | Console-based logging |
207
+ | `JoseJWTService` | `@pikku/jose` | JWT sign/verify via jose |
208
+ | `LocalSecretService` | `@pikku/core/services` | Local development secrets |
209
+ | `LocalVariablesService` | `@pikku/core/services` | Local environment variables |
210
+ | `PinoLogger` | `@pikku/pino` | Structured logging via Pino |
211
+ | `createInvocationAudit` | `@pikku/core/services` | Per-request audit buffer |
212
+ | `createAuditedKysely` | `@pikku/kysely` | Auto-capture DB queries as audit events |
213
+
214
+ ## Complete Example
215
+
216
+ ```typescript
217
+ // services.ts
218
+ import { pikkuServices, pikkuWireServices } from '#pikku/setup'
219
+ import { ConsoleLogger } from '@pikku/core/services'
220
+ import { JoseJWTService } from '@pikku/jose'
221
+
222
+ // Custom service
223
+ class TodoStore {
224
+ private todos: Map<string, Todo> = new Map()
225
+ async create(title: string, priority: string) {
226
+ const todo = { id: crypto.randomUUID(), title, priority, completed: false }
227
+ this.todos.set(todo.id, todo)
228
+ return todo
229
+ }
230
+ async get(id: string) {
231
+ return this.todos.get(id)
232
+ }
233
+ async list() {
234
+ return [...this.todos.values()]
235
+ }
236
+ async delete(id: string) {
237
+ this.todos.delete(id)
238
+ }
239
+ }
240
+
241
+ export const createSingletonServices = pikkuServices(async (config) => {
242
+ const logger = new ConsoleLogger()
243
+ const jwt = new JoseJWTService(
244
+ async () => [{ id: 'my-key', value: config.jwtSecret }],
245
+ logger
246
+ )
247
+ return {
248
+ config,
249
+ logger,
250
+ jwt,
251
+ secrets: new LocalSecretService(),
252
+ variables: new LocalVariablesService(),
253
+ todoStore: new TodoStore(),
254
+ }
255
+ })
256
+
257
+ export const createWireServices = pikkuWireServices(
258
+ async (singletonServices, wire) => ({
259
+ scopedLogger: new ScopedLogger(wire.session?.userId),
260
+ })
261
+ )
262
+
263
+ // functions/todos.functions.ts — services are auto-injected
264
+ export const createTodo = pikkuFunc({
265
+ title: 'Create Todo',
266
+ func: async ({ todoStore, logger }, { title, priority }) => {
267
+ const todo = await todoStore.create(title, priority)
268
+ logger.info('Created todo', { id: todo.id })
269
+ return { todo }
270
+ },
271
+ })
272
+ ```
@@ -74,7 +74,11 @@ pikku-software-archaeology/
74
74
  ├── README.md # this file
75
75
  ├── references/
76
76
  │ ├── blueprint.schema.json # the output contract (JSON Schema)
77
- │ └── pikku-mapping.md # blueprint → Pikku primitives
77
+ │ ├── pikku-mapping.md # blueprint → Pikku primitives
78
+ │ ├── second-opinion.md # blueprint → owner-facing report: method, voice, red flags
79
+ │ └── second-opinion-report-template.md # the layered structure to fill in
80
+ ├── example/
81
+ │ └── second-opinion-sample-report.md # worked example (competitor-tracking area, founder voice)
78
82
  └── scripts/
79
83
  └── validate.mjs # schema + cross-file validation (node, no deps)
80
84
  ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pikku-software-archaeology
3
- description: 'Use when reverse-engineering an existing repository into a Product Blueprint — recovering what product an undocumented or organically-grown codebase implements so it can be rebuilt cleanly (e.g. as a Pikku app). TRIGGER when: user says "extract a blueprint", "reverse engineer this app", "what does this codebase actually do as a product", "prepare this repo for a rewrite/migration", or points at a legacy repo (any language — JS, TS, Ruby, Python, PHP, Go) and asks for its domains, workflows, business rules, or a rebuild plan. DO NOT TRIGGER for: documenting code structure, generating API docs from an already-clean codebase, or code review.'
3
+ description: 'Use when reverse-engineering an existing repository into a Product Blueprint — recovering what product an undocumented or organically-grown codebase implements so it can be rebuilt cleanly (e.g. as a Pikku app) — and when turning that blueprint into a plain-language second opinion for the non-technical owner who holds the app. TRIGGER when: user says "extract a blueprint", "reverse engineer this app", "what does this codebase actually do as a product", "prepare this repo for a rewrite/migration", points at a legacy repo (any language — JS, TS, Ruby, Python, PHP, Go) and asks for its domains, workflows, business rules or a rebuild plan, or asks "explain how my app works" / "what would you do differently" / "is this built well?" for a founder, PM or operator audience. DO NOT TRIGGER for: documenting code structure, generating API docs from an already-clean codebase, or an engineer-facing code review.'
4
4
  ---
5
5
 
6
6
  # Software Archaeology
@@ -132,7 +132,7 @@ Run each lens over the surveyed material. Rules that counter the classic failure
132
132
 
133
133
  **Frontend** (`frontend.json` + `frontend-routes.json` + `frontend-components.json`, optional) — the web UI, which needs its own treatment because frontends vary wildly (framework, router, styling, state, data, auth) and the rebuild target is opinionated: **everything in one component system (Mantine), one data layer, one auth**.
134
134
 
135
- - `frontend.json` records the stack as FACTS: framework (e.g. TanStack Start), rendering (SSR/streaming/SPA), router, styling/design system, `designSystemConsistency`, state management, data layer (e.g. pikku-react-query vs REST helpers), auth (e.g. better-auth), i18n. Name the real technologies — the second-opinion skill weighs their tradeoffs, so record them precisely (do NOT editorialize here; this file is facts).
135
+ - `frontend.json` records the stack as FACTS: framework (e.g. TanStack Start), rendering (SSR/streaming/SPA), router, styling/design system, `designSystemConsistency`, state management, data layer (e.g. pikku-react vs REST helpers), auth (e.g. better-auth), i18n. Name the real technologies — the second-opinion skill weighs their tradeoffs, so record them precisely (do NOT editorialize here; this file is facts).
136
136
  - `frontend-routes.json` is the page tree: each route's `purpose` in product terms, `auth`, the `dataFrom` (query/command names it reads — reuse the backend concept names so the UI ties back to the domain), the `usesComponents`, and the `userFlows` it belongs to.
137
137
  - `frontend.json.designFindings` captures **broken/inconsistent design patterns** as concrete, cited observations (not taste). Actively hunt for: _interaction inconsistency_ (the same job done as a modal in one place and a drawer in another; inconsistent confirm dialogs); _theming not tokenized_ (hardcoded hex colors, magic spacing/font sizes, inline styles instead of theme tokens/variables — grep for `#[0-9a-f]{3,6}`, `style={{`, raw `px` values); _cross-page inconsistency_ (the same element — button, page header, card — styled differently across routes); _component duplication_ (three near-identical cards/tables for one purpose); _design-system bypass_ (raw HTML/CSS where a library component exists). Each finding gets an example, its impact (feels unpolished / a color change means hunting every file), and a fix (standardize on one pattern / move to tokens / extract one shared component). These are almost always cheap cleanups, and they are exactly what a non-technical owner perceives as "the app looks off" without being able to say why.
138
138
  - `frontend-components.json` is where the frontend's real migration cost lives, in the **`rebuild`** field: `mantine-standard` (maps 1:1 to a Mantine component — trivial), `mantine-composition` (built from Mantine primitives — straightforward), `custom-style` (diverges only visually — normalize to Mantine), or **`custom-logic`** (bespoke behavior — a custom chart, a virtualized/complex table, a canvas, drag-and-drop, a rich editor — that must be **ported**, not re-skinned). A `custom-logic` component MUST fill `customLogic` explaining the behavior, and should list the `dependencies` (charting/table/editor libs) that make it a real port. This split — "trivially re-Mantine-able" vs "carries logic that must survive the port" — is the single most useful thing the frontend extraction produces.
@@ -187,3 +187,16 @@ node <skill-dir>/scripts/validate.mjs <repo>/.knowledge
187
187
 
188
188
  - Schema/contract: `references/blueprint.schema.json`
189
189
  - How Pikku consumes the blueprint: `references/pikku-mapping.md`
190
+
191
+ ## The second phase — a report the owner can act on
192
+
193
+ Extraction produces a blueprint for a machine to rebuild from. The same
194
+ `.knowledge/` directory is also the input to a second opinion for the person who
195
+ holds the app: what works, what is holding them back, and what you would build
196
+ instead, argued in business outcomes rather than architecture.
197
+
198
+ That phase has its own voice rules, structure and red flags — read
199
+ `references/second-opinion.md`, fill in
200
+ `references/second-opinion-report-template.md`, and match the tone of
201
+ `example/second-opinion-sample-report.md`. It consumes the blueprint; it never
202
+ re-derives facts from the code, so run the extraction first.
@@ -2,7 +2,7 @@
2
2
 
3
3
  _A second opinion on the competitor-tracking system_
4
4
 
5
- > Worked example for the pikku-product-second-opinion skill. Shows the voice and the
5
+ > Worked example for the pikku-software-archaeology skill. Shows the voice and the
6
6
  > layered structure on one real area (competitor tracking), drawn from a
7
7
  > pikku-software-archaeology blueprint + parity report. A full report would repeat
8
8
  > Part 2 for each major area, and cover every significant technology bet — not just
@@ -961,7 +961,7 @@
961
961
  "stateManagement": { "type": "string" },
962
962
  "dataLayer": {
963
963
  "type": "string",
964
- "description": "how the UI talks to the backend: e.g. pikku-react-query, REST fetch helpers, tRPC, GraphQL client"
964
+ "description": "how the UI talks to the backend: e.g. pikku-react hooks, REST fetch helpers, tRPC, GraphQL client"
965
965
  },
966
966
  "auth": {
967
967
  "type": "string",
@@ -1,6 +1,6 @@
1
1
  # How Pikku Consumes a Product Blueprint
2
2
 
3
- The `.knowledge/` blueprint is designed so each concept maps onto exactly one Pikku primitive. A generator (or an agent following `pikku-feature`) walks the JSON files in this order:
3
+ The `.knowledge/` blueprint is designed so each concept maps onto exactly one Pikku primitive. A generator (or an agent following `pikku-build`) walks the JSON files in this order:
4
4
 
5
5
  | Blueprint source | Pikku target |
6
6
  | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -26,8 +26,8 @@ The `.knowledge/` blueprint is designed so each concept maps onto exactly one Pi
26
26
  | `interfaces.json` kind=cli | `wireCLI` entrypoints — the CLI commands are the same funcs the routes expose |
27
27
  | `interfaces.json` kind=mcp | `wireMCP` — each MCP tool IS a `pikkuFunc` (reuse the command/query funcs; don't author tool duplicates) |
28
28
  | `interfaces.json` kind=openapi-rest / sdk | generated, not hand-written: the OpenAPI spec + typed client SDK fall out of the `wireHTTP` routes + codegen |
29
- | `interfaces.json` kind=websocket-realtime | `pikku-realtime` EventHub topics / channels |
30
- | `frontend.json` | `apps/app` (TanStack Start) shell: router, `@pikku/mantine` theme, `pikku-react-query` data layer, `better-auth` client — the target stack the legacy UI is rebuilt onto |
29
+ | `interfaces.json` kind=websocket-realtime | `pikku-wiring` EventHub topics / channels |
30
+ | `frontend.json` | `apps/app` (TanStack Start) shell: router, `@pikku/mantine` theme, `pikku-react` data layer, `better-auth` client — the target stack the legacy UI is rebuilt onto |
31
31
  | `frontend-routes.json` | TanStack Router routes under `apps/app/src/routes/**` (thin data containers calling `usePikkuQuery`); `dataFrom` names become the generated hooks; subpath routes for rich detail views |
32
32
  | `frontend-components.json` rebuild=`mantine-standard`/`mantine-composition` | components in `packages/components` composed from `@pikku/mantine` — the trivial/straightforward bulk |
33
33
  | `frontend-components.json` rebuild=`custom-logic` | the PORT list — each becomes a `packages/components` component that reimplements the bespoke behavior (chart/table/editor); its `dependencies` inform whether the lib is kept or replaced. These are the frontend's real work items |
@@ -1,8 +1,3 @@
1
- ---
2
- name: pikku-product-second-opinion
3
- description: 'Use when a non-technical owner (founder, PM, operator) wants a plain-language report on an app they hold but did not build — explaining how it works and how it could be better. Reads the .knowledge/ blueprint from pikku-software-archaeology and produces a layered, jargon-free report that credits what works, names what does not (with business impact + effort), and argues an opinionated better design. TRIGGER: "explain how my app works", "what would you do differently", "review my app for a non-technical audience", "I inherited/am stuck with an agency-built app", "is this built well?". DO NOT TRIGGER for: extracting the machine-readable blueprint itself (use pikku-software-archaeology), or an engineer-facing technical code review.'
4
- ---
5
-
6
1
  # Product Second Opinion
7
2
 
8
3
  ## Overview
@@ -11,7 +6,7 @@ Turn an extracted product blueprint into a **report a non-technical owner can ac
11
6
 
12
7
  The reader is a founder/PM/operator, not an engineer. If they finish a section and don't know what it means for their business or what to do about it, the report failed — no matter how correct it is.
13
8
 
14
- **REQUIRED INPUT:** the `.knowledge/` blueprint produced by **pikku-software-archaeology**. If none exists, run that skill first — this one consumes its output (`product.json`, `domains.json`, `workflows.json`, `gaps.json`, `invariants.json`, `migration.json`, and any `parity-*.md`), it does not re-derive facts from the code. When the optional consumer-surface files are present (`interfaces.json`, `frontend.json`, `frontend-routes.json`, `frontend-components.json`), cover them too — see "The frontend and the other ways your app is used" and "Technology choices" below.
9
+ **REQUIRED INPUT:** the `.knowledge/` blueprint produced by the extraction phase (see SKILL.md). If none exists, run that first — this one consumes its output (`product.json`, `domains.json`, `workflows.json`, `gaps.json`, `invariants.json`, `migration.json`, and any `parity-*.md`), it does not re-derive facts from the code. When the optional consumer-surface files are present (`interfaces.json`, `frontend.json`, `frontend-routes.json`, `frontend-components.json`), cover them too — see "The frontend and the other ways your app is used" and "Technology choices" below.
15
10
 
16
11
  ## The cardinal rule: translate, don't dump
17
12
 
@@ -52,7 +47,7 @@ Write these three parts in order. A reader can stop after Part 1.
52
47
  - _The headline_: the 3–5 biggest risks/opportunities, one plain line each.
53
48
  - _Recommended order_: a table (Fix | Why it matters | Effort | Payoff). This is the part they act on.
54
49
 
55
- **Part 2 — One section per major area** (drive the areas from `domains.json`; skip domains with nothing worth saying). Each section follows this shape (see `example/sample-report.md`):
50
+ **Part 2 — One section per major area** (drive the areas from `domains.json`; skip domains with nothing worth saying). Each section follows this shape (see `example/second-opinion-sample-report.md`):
56
51
 
57
52
  - _What this does_ — the capability in business terms.
58
53
  - _How it works today_ — a plain walkthrough, ideally as a small story ("on a timer, the app re-reads each site, compares…").
@@ -162,6 +157,6 @@ Produce **both**:
162
157
  | A design point with no "why it matters" | Taste, not advice. Tie every design finding to user perception (polish/trust) or maintenance cost (change-once vs hunt-everywhere), plus effort. |
163
158
  | Reads like a code review | Wrong audience. Would a founder know what to _do_ after this paragraph? |
164
159
 
165
- ## Relationship to pikku-software-archaeology
160
+ ## Relationship to the extraction phase
166
161
 
167
- `pikku-software-archaeology` = facts → `.knowledge/` blueprint, for a machine to rebuild from. **This skill** = blueprint → opinionated report, for a human to decide from. One extracts; one advises. Run archaeology first (or point this skill at an existing `.knowledge/`), then translate its `gaps.json` + `invariants.json` + `migration.json` into the business-language report above.
162
+ Extraction = facts → `.knowledge/` blueprint, for a machine to rebuild from. This report = blueprint → opinionated argument, for a human to decide from. One extracts; one advises. Run the extraction first (or point this at an existing `.knowledge/`), then translate its `gaps.json` + `invariants.json` + `migration.json` into the business-language report above.
@@ -0,0 +1,199 @@
1
+ ---
2
+ name: pikku-webhook
3
+ description: >-
4
+ Use when an application needs to SEND outgoing webhooks — notifying a customer's endpoint that
5
+ something happened, with signing, retries and a delivery log. Covers WebhookService,
6
+ QueueWebhookService, the `pikku-outgoing-webhooks` queue worker, `scaffold.webhook`,
7
+ `config.webhook`, signature verification on the receiving side, and KyselyWebhookService's
8
+ delivery history. TRIGGER when: code uses webhookService, QueueWebhookService,
9
+ KyselyWebhookService, pikkuWebhookWorkerFunc, PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME,
10
+ SendWebhookInput or X-Pikku-Signature. TRIGGER when: the user asks to emit events to a
11
+ customer URL, build a webhook endpoint settings screen, add a signing secret, or show
12
+ delivery attempts. DO NOT TRIGGER when: the user is RECEIVING webhooks from a third party
13
+ into a route — that is an ordinary wireHTTP function (use pikku-wiring).
14
+ installGroups: [core]
15
+ ---
16
+
17
+ # Pikku Outgoing Webhooks
18
+
19
+ Pikku ships an outgoing webhook primitive, so an application never hand-rolls
20
+ `fetch` + HMAC + retries. `WebhookService.send()` signs the body, enqueues a
21
+ delivery job, and the generated `pikku-outgoing-webhooks` queue worker POSTs it;
22
+ a non-2xx throws, so the queue retries with backoff. Swapping the queue-only
23
+ default for `KyselyWebhookService` adds a durable delivery + attempt history
24
+ with no change to call sites.
25
+
26
+ **Do not write a bespoke `fetch(url, { headers: { 'x-my-signature': … } })` for
27
+ an outgoing event.** If the shipped signing scheme or delivery model genuinely
28
+ does not fit, subclass `WebhookService` — it is an abstract class precisely so
29
+ an app can substitute its own transport (direct send, Svix) and keep the same
30
+ call sites and delivery history.
31
+
32
+ ## Agent Operating Procedure
33
+
34
+ 1. Turn the feature on: `"scaffold": { "webhook": true }` in `pikku.config.json`.
35
+ 2. Run `pikku all`. It writes `<scaffold.pikkuDir>/webhook/webhook.gen.ts` (the
36
+ queue worker) and `webhook.schemas.gen.ts`. Never hand-edit either.
37
+ 3. Register a `webhookService` singleton in `createSingletonServices`. Without
38
+ it `services.webhookService` is `undefined` and nothing sends.
39
+ 4. Make sure a queue backend is wired. The worker is an ordinary
40
+ `wireQueueWorker` — with no `queueService`, `send()` throws.
41
+ 5. Call `webhookService.send(...)` from a function body. Never call `fetch`
42
+ directly for an outgoing event.
43
+ 6. Validate with `pikku all` and the project's typecheck.
44
+
45
+ ## Config
46
+
47
+ ```jsonc
48
+ // pikku.config.json
49
+ {
50
+ "scaffold": {
51
+ "pikkuDir": "src/pikku",
52
+ "webhook": true, // on or off — it exposes no endpoint and has no path override
53
+ },
54
+ }
55
+ ```
56
+
57
+ `scaffold.webhook` is a plain boolean, unlike the other scaffold flags which
58
+ accept `{ path }`. The two generated paths are derived
59
+ (`webhookWorkersFile`, `webhookSchemasFile`) and can be set explicitly in
60
+ `pikku.config.json` if a project needs them somewhere else.
61
+
62
+ Runtime defaults live on `CoreConfig.webhook`:
63
+
64
+ ```ts
65
+ export interface Config extends CoreConfig {}
66
+
67
+ const config: Config = {
68
+ webhook: {
69
+ secret: 'WEBHOOK_SIGNING_KEY', // a secret NAME, resolved via services.secrets
70
+ signatureHeader: 'X-Pikku-Signature', // default
71
+ retries: 3, // default; attempts = retries + 1
72
+ retryDelay: '30s', // omit for exponential backoff
73
+ allowedHosts: ['hooks.example.com'], // SSRF allowlist
74
+ },
75
+ }
76
+ ```
77
+
78
+ `allowedHosts` set means _only_ those hostnames are deliverable. Omitted, every
79
+ public host is allowed and private/internal ranges are blocked — loopback,
80
+ RFC1918, link-local `169.254.0.0/16` (cloud metadata), CGNAT `100.64.0.0/10`
81
+ (Alibaba's metadata endpoint), multicast and the TEST-NETs. A URL is
82
+ user-supplied data; do not bypass `safeFetch` by delivering yourself.
83
+
84
+ ## Sending
85
+
86
+ ```ts
87
+ import { pikkuFunc } from '#pikku/function'
88
+
89
+ export const notifyOrderShipped = pikkuFunc({
90
+ func: async ({ webhookService }, { endpointUrl, orderId, secret }) => {
91
+ const { jobId } = await webhookService.send({
92
+ url: endpointUrl,
93
+ event: 'order.shipped',
94
+ data: { orderId, shippedAt: new Date().toISOString() },
95
+ secret, // per-endpoint raw key; overrides config.webhook.secret
96
+ organizationId: orgId, // persisted by store-backed services only
97
+ })
98
+ return { jobId }
99
+ },
100
+ })
101
+ ```
102
+
103
+ `send()` returns as soon as the job is enqueued — it is **not** a delivery
104
+ receipt. `jobId` is the queue job; with `KyselyWebhookService` it is also the
105
+ `deliveryId`, so it is stable across retries and is what a UI polls.
106
+
107
+ The two `secret` fields are deliberately different and are the most common
108
+ mistake:
109
+
110
+ | Where | Meaning |
111
+ | ------------------------- | ------------------------------------------------------------------- |
112
+ | `config.webhook.secret` | a secret **name**, read through `services.secrets` at enqueue time |
113
+ | `SendWebhookInput.secret` | a **raw HMAC key**, for per-endpoint secrets held in your own table |
114
+
115
+ The raw key never enters the queue payload: the body is signed at enqueue time
116
+ and only the resulting header travels with the job. That is also why the body
117
+ is serialized once — a retry re-POSTs identical bytes, so the signature stays
118
+ valid.
119
+
120
+ With neither secret set, deliveries go **unsigned**. A missing named secret is
121
+ logged as an error and still sends unsigned; treat that log line as a
122
+ misconfiguration, not noise.
123
+
124
+ ## Verifying on the receiving side
125
+
126
+ `sign()` produces `sha256=<hex>` (GitHub style, body only, no timestamp) into
127
+ `X-Pikku-Signature`, and `X-Pikku-Event` carries the event name. `verify()` is
128
+ public because receivers share the scheme:
129
+
130
+ ```ts
131
+ const raw = await request.text()
132
+ if (
133
+ !webhookService.verify(secret, request.headers.get('x-pikku-signature')!, raw)
134
+ ) {
135
+ throw new UnauthorizedError()
136
+ }
137
+ ```
138
+
139
+ It compares in constant time via `timingSafeStringEqual`. Never compare
140
+ signatures with `===`, and verify against the **raw body text**, not a
141
+ re-serialized parsed object.
142
+
143
+ ## Delivery history
144
+
145
+ The default `QueueWebhookService` keeps no history: `listDeliveries`,
146
+ `getDelivery` and `recordAttempt` throw `NotImplementedError`. Register
147
+ `KyselyWebhookService` from `@pikku/kysely` to get them.
148
+
149
+ ```ts
150
+ import { KyselyWebhookService } from '@pikku/kysely'
151
+
152
+ const webhookService = new KyselyWebhookService(queueService, kysely)
153
+ await webhookService.init() // creates webhookDelivery + webhookDeliveryAttempt
154
+ ```
155
+
156
+ `init()` is idempotent and creates the tables through the pikku schema
157
+ bootstrap. Do not write your own migration for these tables.
158
+
159
+ - `webhookDelivery` — one row per `send()`: `deliveryId`, `organizationId`,
160
+ `url`, `event`, `status` (`pending` | `delivered` | `failed`), `attempts`,
161
+ `createdAt`, `updatedAt`, `deliveredAt`.
162
+ - `webhookDeliveryAttempt` — one row per try: `attemptNumber`, `statusCode`,
163
+ `responseBody` (failures only, truncated to 2000 chars), `error`.
164
+
165
+ Read them through the service, not with your own query:
166
+
167
+ ```ts
168
+ const deliveries = await webhookService.listDeliveries({
169
+ organizationId,
170
+ limit: 25,
171
+ })
172
+ const detail = await webhookService.getDelivery(deliveryId) // { delivery, attempts }
173
+ ```
174
+
175
+ Building a console screen is `listDeliveries` for the list and `getDelivery` for
176
+ the drill-in. A hand-written select over `webhookDelivery` is a sign the wrong
177
+ service is registered.
178
+
179
+ ## What the worker does
180
+
181
+ The generated worker is a thin wrapper over `pikkuWebhookWorkerFunc`. It POSTs
182
+ through `safeFetch` with a 30s timeout, treats 2xx as delivered, captures the
183
+ response body on failure, records the attempt when a `deliveryId` is present,
184
+ and **throws** on failure so the queue retries. Attempt recording is
185
+ best-effort: a store error is logged and does not fail the delivery. Retry
186
+ exhaustion is logged by the queue runner — there is no `onFailure` hook.
187
+
188
+ ## Gotchas
189
+
190
+ - `webhookService` is optional on `CoreSingletonServices`, but do **not** guard
191
+ it in a function body — destructuring it marks it required (see
192
+ `pikku-services`). Register it in `services.ts` or fail fast at startup.
193
+ - The queue name is `pikku-outgoing-webhooks`, not `pikku-webhooks`.
194
+ - `retries: 0` means one attempt and no backoff, not "retry forever".
195
+ - The gateway's inbound `webhook` transport type is a different feature. This
196
+ skill is outbound only.
197
+ - Signing is body-only with no timestamp, so it does not defend against replay
198
+ on its own. If a receiver needs replay protection, put a nonce or timestamp
199
+ **inside** the payload, where it is covered by the signature.