@pikku/skills 0.12.19 → 0.12.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.19",
3
+ "version": "0.12.21",
4
4
  "description": "The Pikku agent skills — the instruction set coding agents read to build, wire and deploy Pikku projects",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
@@ -237,12 +237,12 @@ Banning is the one capability with a schema requirement, and it has its own
237
237
  small plugin:
238
238
 
239
239
  ```typescript
240
- import { ban } from '@pikku/better-auth'
240
+ import { pikkuBan } from '@pikku/better-auth'
241
241
 
242
- betterAuth({ plugins: [ban()] })
242
+ betterAuth({ plugins: [pikkuBan()] })
243
243
  ```
244
244
 
245
- `ban()` adds `banned`, `banReason` and `banExpires` to `user` and refuses to
245
+ `pikkuBan()` adds `banned`, `banReason` and `banExpires` to `user` and refuses to
246
246
  create a session for a banned user, lapsing an expired ban as it goes. It makes
247
247
  no authorization decision — who may ban is decided by `admin:users:ban` — so it
248
248
  never needs to know about scopes or roles.
@@ -252,22 +252,28 @@ never needs to know about scopes or roles.
252
252
  Five, all imported from the package root and passed to `betterAuth({ plugins })`
253
253
  like any other. None is automatic — an app wires the ones it needs.
254
254
 
255
- | Plugin | Plugin `id` | Adds | Use it when |
256
- | ------------------- | ------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------- |
257
- | `ban()` | `pikku-ban` | `user.banned/banReason/banExpires` | You ban users (the schema + enforcement half of the above) |
258
- | `actor()` | `actor` | `POST /sign-in/actor`, `user.actor` | Scenarios or a dev switcher sign in as a persona |
259
- | `credentialOAuth()` | `credential-oauth` | `POST /credential-oauth/link`, `/credential-oauth/callback/:providerId` | An app links OAuth2 **API credentials** for a user |
260
- | `delegatedAuth()` | `delegated-auth` | `POST /sign-in/delegated` | An imported upstream API is the system of record for identity |
261
- | `fabric()` | `fabric` | `POST /sign-in/fabric` | A Fabric-deployed app lets a control-plane operator in |
255
+ | Plugin | Plugin `id` | Adds | Use it when |
256
+ | ------------------------ | ------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------- |
257
+ | `pikkuBan()` | `pikku-ban` | `user.banned/banReason/banExpires` | You ban users (the schema + enforcement half of the above) |
258
+ | `pikkuActor()` | `actor` | `POST /sign-in/actor`, `user.actor` | Scenarios or a dev switcher sign in as a persona |
259
+ | `pikkuCredentialOAuth()` | `credential-oauth` | `POST /credential-oauth/link`, `/credential-oauth/callback/:providerId` | An app links OAuth2 **API credentials** for a user |
260
+ | `pikkuDelegatedAuth()` | `delegated-auth` | `POST /sign-in/delegated` | An imported upstream API is the system of record for identity |
261
+ | `pikkuFabric()` | `fabric` | `POST /sign-in/fabric` | A Fabric-deployed app lets a control-plane operator in |
262
+
263
+ Every one carries a `pikku` prefix, because a `plugins: [...]` array mixes these
264
+ with better-auth's own and a bare `actor()` next to `organization()` says
265
+ nothing about where it came from. The unprefixed names — `ban`, `actor`,
266
+ `credentialOAuth`, `delegatedAuth`, `fabric` — are still exported as deprecated
267
+ aliases, so existing apps keep working.
262
268
 
263
269
  The plugin's `id` is what better-auth stores; the **export name** is what the
264
270
  inspector reads off your `plugins` array and what generated metadata is keyed
265
- by, so the two differ for `ban` and `delegatedAuth`.
271
+ by, so the two differ for every one of them.
266
272
 
267
- #### `credentialOAuth()` — link API credentials, not identities
273
+ #### `pikkuCredentialOAuth()` — link API credentials, not identities
268
274
 
269
275
  ```typescript
270
- credentialOAuth({
276
+ pikkuCredentialOAuth({
271
277
  config: [
272
278
  {
273
279
  providerId: 'github',
@@ -308,10 +314,10 @@ by construction. Tokens land in better-auth's `account` table, so
308
314
  An undeclared `providerId` is a 404; an anonymous caller a 401; a refused
309
315
  singleton a 403 that leaves no platform user behind.
310
316
 
311
- #### `delegatedAuth()` — the upstream API is the identity provider
317
+ #### `pikkuDelegatedAuth()` — the upstream API is the identity provider
312
318
 
313
319
  ```typescript
314
- delegatedAuth({
320
+ pikkuDelegatedAuth({
315
321
  authenticate: async ({ email, password, apiKey }) => upstream.login(...),
316
322
  storeCredential: (userId, identity) =>
317
323
  credentialService.set('acme', identity.credential, userId),
@@ -338,10 +344,10 @@ a warning and the user still gets in.
338
344
  `storeCredential` failing, by contrast, **fails the sign-in**: every proxied
339
345
  call would be dead anyway.
340
346
 
341
- #### `fabric()` — control-plane operator sign-in
347
+ #### `pikkuFabric()` — control-plane operator sign-in
342
348
 
343
349
  ```typescript
344
- fabric({ publicKey: FABRIC_AUTH_PUBLIC_KEY, scopeService, logger })
350
+ pikkuFabric({ publicKey: FABRIC_AUTH_PUBLIC_KEY, scopeService, logger })
345
351
  ```
346
352
 
347
353
  `POST /sign-in/fabric` verifies a short-lived RS256 token that the Fabric
@@ -468,14 +474,27 @@ someone" means **a particular kind of user** rather than one fixed admin.
468
474
  Register it explicitly — it is not automatic:
469
475
 
470
476
  ```typescript
471
- import { actor } from '@pikku/better-auth'
477
+ import { pikkuActor } from '@pikku/better-auth'
472
478
 
473
- plugins: [actor({ secret: SCENARIO_ACTOR_SECRET })]
479
+ plugins: [pikkuActor({ secret: SCENARIO_ACTOR_SECRET })]
474
480
  ```
475
481
 
476
482
  `POST ${basePath}/sign-in/actor` `{ email, secret, name? }` → 200 + the normal
477
- session cookie. `secret` may also be a (possibly async) function, so it can come
478
- off the secrets service instead of a captured value.
483
+ session cookie. The plugin's `secret` is the **root**, and it may be a (possibly
484
+ async) function so it can come off the secrets service instead of a captured
485
+ value.
486
+
487
+ **What a caller presents is not the root.** It is
488
+ `deriveActorSecret(root, email)` from `@pikku/core/services` — HKDF-expanded
489
+ HMAC-SHA256 over the lowercased address. The endpoint re-derives the expected
490
+ value for the address being signed in as and compares, so a credential minted
491
+ for one persona is refused for every other, and the root itself is never a valid
492
+ credential. A root under 32 characters refuses the endpoint outright rather than
493
+ deriving weak credentials from it (the server log names the problem; the client
494
+ is not told which). Callers rarely derive by hand — `pikku dev` mints one per
495
+ persona into `VITE_DEV_ACTOR_SECRETS` for the browser switcher, `pikku persona
496
+ secret <id>` mints them for a run, and the two `PersonaSignIn` implementations
497
+ derive on the fly.
479
498
 
480
499
  **Which command is running decides whether it works, not whether a secret is
481
500
  set.** `pikku dev` sets `PIKKU_DEV_ACTOR_SIGN_IN` and mints an ephemeral
@@ -516,8 +535,10 @@ actor`. So the secret cannot take over a **real user's** account — the blast
516
535
  - **Unknown emails are created only under `pikku dev`**, flagged `actor: true`,
517
536
  so a local scenario declaring a new persona needs no seed step. Anywhere else
518
537
  the account has to have been provisioned at boot first.
519
- - **The comparison is constant-time and length-hiding**, so a wrong secret leaks
520
- neither the length nor a prefix of the right one.
538
+ - **A credential is bound to one address**, so a leaked one is one synthetic
539
+ account rather than the whole actor population; only the root is worth the
540
+ paragraph above. The comparison is constant-time and length-hiding, so a wrong
541
+ credential leaks neither the length nor a prefix of the right one.
521
542
 
522
543
  This is the endpoint `pikku scenario` signs its actors in through, and the one
523
544
  the frontend dev switcher posts to — see `pikku-scenario` for declaring the
@@ -570,7 +591,7 @@ await provisionPersonas(singletonServices, {
570
591
  ```
571
592
 
572
593
  It writes the same `banned` column the console's ban RPC writes (so it needs the
573
- `ban()` plugin wired, and says so if it isn't), revokes the account's sessions,
594
+ `pikkuBan()` plugin wired, and says so if it isn't), revokes the account's sessions,
574
595
  and leaves the row, its grants and its history intact — provisioning lifts the
575
596
  ban again by itself if the persona comes back. Deleting is deliberately not
576
597
  offered: an actor row is referenced by whatever those scenarios did while it
@@ -28,7 +28,7 @@ project shaped so `pikku fabric init` later adopts it with zero rework.
28
28
  ## Agent Operating Procedure
29
29
 
30
30
  1. Discover before editing. Run `pikku info functions --verbose --silent` and
31
- read `AGENTS.md` before your first change. Read `locale` in
31
+ read `AGENTS.md` before your first change. Read `metaLocale` in
32
32
  `pikku.config.json` too: it is the language every `description`, `title` and
33
33
  step `template` you write must be in (§1a). Identifiers stay English whatever
34
34
  it says.
@@ -92,21 +92,21 @@ settle all three explicitly before you write code.
92
92
  | Axis | What it covers | Where it goes |
93
93
  | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
94
94
  | **Identifiers** | Function, component, type, variable and file names. Database tables and columns. Commit messages. | Nowhere — **always English**, no setting, not negotiable |
95
- | **Meta** | `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role and persona descriptions | `locale` in `pikku.config.json`, default `en` |
95
+ | **Meta** | `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role and persona descriptions | `metaLocale` in `pikku.config.json`, default `en` |
96
96
  | **Product UI** | Every string the app shows a user | `messages/<locale>.json`, and `defaultLocale` for what a first-time visitor opens in |
97
97
 
98
98
  **Identifiers are English.** The product's market does not change this and
99
- neither does `locale`. Identifiers are the surface the generated `#pikku/*`
99
+ neither does `metaLocale`. Identifiers are the surface the generated `#pikku/*`
100
100
  clients, `pikku info`, the typed RPC map and the Kysely types all bind to, and
101
101
  unlike a string an identifier cannot be translated later — renaming one is a
102
102
  migration. A German practice management tool gets `getWorklist`, `case`,
103
103
  `event`, not `getUebersicht`, `vorgang`, `ereignis`.
104
104
 
105
- **Meta follows `locale`.** Write the team's answer into `pikku.config.json` in
105
+ **Meta follows `metaLocale`.** Write the team's answer into `pikku.config.json` in
106
106
  this phase, before there is any meta to be wrong:
107
107
 
108
108
  ```json
109
- { "locale": "de" }
109
+ { "metaLocale": "de" }
110
110
  ```
111
111
 
112
112
  It exists for the Pikku Console. Meta is the one part of a project the Console
@@ -114,7 +114,7 @@ renders back to a human, so a team working in German reads their own functions,
114
114
  features and scenario reports in German. Default `en` and do not ask when the
115
115
  project is obviously English. **On every later run, read this field first and
116
116
  author descriptions, titles and step templates in it** — a project whose
117
- `locale` you ignored reports half in one language and half in another.
117
+ `metaLocale` you ignored reports half in one language and half in another.
118
118
 
119
119
  **Product UI is the message catalogue.** `messages/<locale>.json` via
120
120
  `pikku-i18n`, with `defaultLocale` deciding what a visitor opens in.
@@ -131,7 +131,7 @@ A German medical portal, correctly:
131
131
  // apps/app/src/i18n/active.json (or: fabric i18n --default-locale de)
132
132
  { "defaultLocale": "de" }
133
133
  // pikku.config.json
134
- { "locale": "de" }
134
+ { "metaLocale": "de" }
135
135
  ```
136
136
 
137
137
  Record the two non-obvious answers as a `decisions/` note in §2 — neither is
@@ -464,8 +464,16 @@ built-in module: node:sqlite`.
464
464
 
465
465
  Open it, sign up, and click through what you built. **HTTP 200 is not evidence.**
466
466
  The pages are client-rendered: the server returns 200 with an empty shell, so a
467
- page whose component throws still looks fine to `curl`. Open it in a browser, or
468
- drive it headlessly and assert on the text that actually rendered.
467
+ page whose component throws still looks fine to `curl`.
468
+
469
+ That click-through is a smoke check, and it is the only thing it is. **Do not
470
+ hand-drive a browser tool in place of a test.** Steering Playwright yourself
471
+ proves a page rendered once, on your machine, in an order only you remember —
472
+ nothing about it re-runs, so the next agent inherits a claim rather than a test,
473
+ and a regression lands silently. When a journey is worth driving through the UI,
474
+ it is worth writing as a browser step on §7's scenario and running
475
+ `pikku scenario run local --spawn --run browser`: same clicks, same assertions,
476
+ in the repo, green or red on every future run.
469
477
 
470
478
  ## 7. Prove it — scenarios
471
479
 
@@ -158,7 +158,7 @@ that is the finding.
158
158
  every added locale is cloned from, so three locales is `locales: ["en", …]` and
159
159
  never a repointed base. Shipping locales is also not a reason for anything in
160
160
  the code to stop being English: identifiers are English in every project, and
161
- the language of `description`/`title`/`template` is `locale` in
161
+ the language of `description`/`title`/`template` is `metaLocale` in
162
162
  `pikku.config.json`. See `pikku-build-app` §1a.
163
163
 
164
164
  ### Emails — `pikku-emails`
@@ -54,7 +54,7 @@ defaults are the point.
54
54
 
55
55
  Do not ask about language either — take the defaults and note them in §6.
56
56
  Identifiers are English in every project, whatever the product's market;
57
- `locale` in `pikku.config.json` (the language of `description`/`title`/step
57
+ `metaLocale` in `pikku.config.json` (the language of `description`/`title`/step
58
58
  `template`, which the Console renders) stays `en` unless the user already told
59
59
  you otherwise. If the request says the app's UI is not English, that is the
60
60
  message catalogue only: add the locale and set `defaultLocale`, and leave
@@ -178,8 +178,11 @@ exactly like an app bug, so if every request fails, check both came up.
178
178
 
179
179
  Sign up, click every screen. **HTTP 200 is not evidence:** pages are
180
180
  client-rendered, so the server returns 200 with an empty shell and a page whose
181
- component throws still looks fine to `curl`. Open a browser, or drive it
182
- headlessly and assert on rendered text.
181
+ component throws still looks fine to `curl`.
182
+
183
+ Looking is for the layout — the part only eyes catch. Assertions belong in the
184
+ smoke scenario, not in a browser session you steered by hand: that session proves
185
+ a screen rendered once, here, and nothing about it re-runs.
183
186
 
184
187
  **Screenshot at 390px too.** A layout that is fine at 1440 routinely breaks on a
185
188
  phone — an overflowing table, a row of buttons wrapped into a pile, a modal
@@ -138,7 +138,7 @@ Services can be destructured inline in the `func` signature (e.g. `async ({ logg
138
138
 
139
139
  ```typescript
140
140
  pikkuFunc({
141
- // Identity and documentation — prose, so it follows `locale` in
141
+ // Identity and documentation — prose, so it follows `metaLocale` in
142
142
  // pikku.config.json (default `en`). The identifier does not; see
143
143
  // "What Language You Write In".
144
144
  title?: string, // Human-readable name
@@ -333,12 +333,12 @@ bottom.
333
333
  | Axis | What it covers | What decides it |
334
334
  | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
335
335
  | **Identifiers** | Function, component, type, variable and file names. Database tables and columns. Branch names and commit messages. | Nothing. **Always English.** There is no setting. |
336
- | **Meta** | The prose authored _inside_ the code: `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role/persona descriptions. | `locale` in `pikku.config.json`. Defaults to `en`. |
336
+ | **Meta** | The prose authored _inside_ the code: `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role/persona descriptions. | `metaLocale` in `pikku.config.json`. Defaults to `en`. |
337
337
  | **Product UI** | Every string the app shows a user. | `messages/<locale>.json`, with `active.json`'s `defaultLocale` choosing what a first-time visitor opens in. |
338
338
 
339
339
  ### Identifiers are English, and nothing changes that
340
340
 
341
- Not the product's market, not the team's working language, and **not `locale`**.
341
+ Not the product's market, not the team's working language, and **not `metaLocale`**.
342
342
  A German medical practice, an Arabic marketplace and a Japanese logistics tool
343
343
  all get `getOverview`, `AttentionStripe`, `case`, `event`.
344
344
 
@@ -350,10 +350,10 @@ A `vorgang` table types as `Vorgang` in Kysely and reads as noise to everyone wh
350
350
  did not name it, and unlike a string it cannot be translated later — renaming an
351
351
  identifier is a migration, not an edit.
352
352
 
353
- ### Meta follows `locale`, and that is what the field is for
353
+ ### Meta follows `metaLocale`, and that is what the field is for
354
354
 
355
355
  ```json
356
- { "locale": "de" }
356
+ { "metaLocale": "de" }
357
357
  ```
358
358
 
359
359
  Meta is the one part of a project the **Pikku Console** renders back to a human.
@@ -366,7 +366,7 @@ Read it before you author meta, and write descriptions, titles and templates in
366
366
  it. Absent, it is `en`. It is a BCP-47 tag (`en`, `de`, `pt-BR` — a hyphen, not
367
367
  an underscore), and the CLI rejects anything else by name.
368
368
 
369
- `locale` is **not** licence to rename anything. `locale: "de"` buys a German
369
+ `metaLocale` is **not** licence to rename anything. `metaLocale: "de"` buys a German
370
370
  `description: 'Zeigt die Arbeitsliste'` on a function still called
371
371
  `getWorklist`.
372
372
 
@@ -405,7 +405,7 @@ settings, each on its own axis:
405
405
  { "defaultLocale": "de" }
406
406
 
407
407
  // pikku.config.json — the language the team reads their Console in
408
- { "locale": "de" }
408
+ { "metaLocale": "de" }
409
409
  ```
410
410
 
411
411
  Identifiers stay English throughout. When a brief tells you the product speaks a
@@ -449,7 +449,7 @@ src/
449
449
 
450
450
  // Use tags for cross-cutting concerns:
451
451
  wireHTTP({ route: '/todos', func: listTodos, tags: ['todos', 'public'] })
452
- addMiddleware('todos', [loggingMiddleware]) // Applies to all 'todos'-tagged functions
452
+ addTagMiddleware('todos', [loggingMiddleware]) // Applies to all 'todos'-tagged functions
453
453
  ```
454
454
 
455
455
  ---
@@ -129,8 +129,8 @@ a stage whose pages return 200 — the shell renders fine — while its first da
129
129
  read throws `no result` or a foreign-key violation on a row the seed was
130
130
  silently supplying.
131
131
 
132
- A Better Auth app has a second constraint: the plugins you enable (`ban()`,
133
- `actor()`, …) each declare columns, and `pikku db migrate` refuses to run while
132
+ A Better Auth app has a second constraint: the plugins you enable (`pikkuBan()`,
133
+ `pikkuActor()`, …) each declare columns, and `pikku db migrate` refuses to run while
134
134
  the applied schema is missing any of them. `pikku db generate` writes the
135
135
  migration that closes the gap.
136
136
 
@@ -268,8 +268,12 @@ bun run dev
268
268
  Then open the app, sign up as a real user, and click through what you built.
269
269
  **HTTP 200 is not evidence.** These are client-rendered pages: the server returns
270
270
  200 with an empty shell, so a page whose component throws still looks fine to
271
- curl. Either open it in a browser or drive it headlessly and assert on rendered
272
- text.
271
+ curl.
272
+
273
+ That pass is a smoke check. Anything you would otherwise verify by hand-driving a
274
+ browser tool belongs in a scenario's browser step, run with
275
+ `pikku scenario run local --spawn --run browser` — a browser session you steered
276
+ yourself proves nothing that re-runs.
273
277
 
274
278
  Secrets come from `process.env`, which the CLI populates from a `.env` in the
275
279
  working directory. `BETTER_AUTH_SECRET` is required — without it the first
@@ -416,7 +420,7 @@ These apply in every Fabric app:
416
420
  - **No hand-editing `.pikku/db/schema.gen.ts`** — write a migration and re-run `pikku db migrate`.
417
421
  - **One runtime unit per file** — never define multiple functions/workflows in a single source file.
418
422
  - **Workflow steps don't need manual wiring** — `pikkuSessionlessFunc` step functions in `*.steps.ts` files are auto-discovered by codegen.
419
- - **Identifiers are English** — functions, components, types, files, database tables and columns, in every app whatever market it serves. The team's language is `locale` in `pikku.config.json` and reaches `description`/`title`/`template` only; the app's language is the message catalogue, where `baseLocale` stays `en` and `defaultLocale` decides what a visitor opens in. `pikku fabric validate` warns (`app-base-locale-not-english-<app>`) when an app repoints its base.
423
+ - **Identifiers are English** — functions, components, types, files, database tables and columns, in every app whatever market it serves. The team's language is `metaLocale` in `pikku.config.json` and reaches `description`/`title`/`template` only; the app's language is the message catalogue, where `baseLocale` stays `en` and `defaultLocale` decides what a visitor opens in. `pikku fabric validate` warns (`app-base-locale-not-english-<app>`) when an app repoints its base.
420
424
 
421
425
  ## Converting an existing app to Fabric format
422
426
 
@@ -432,7 +436,7 @@ Fix every `error` and `warn` in the output before continuing. Then:
432
436
  2. **Replace route handlers with pikkuFuncs**: extract business logic into `pikkuFunc`/`pikkuSessionlessFunc`, add `wireHTTP` or `expose: true` for transport.
433
437
  3. **Replace DI/IoC with pikkuServices**: move service construction to `createSingletonServices` in `services.ts`.
434
438
  4. **Replace `process.env` calls**: plain config becomes `defineVariable` + `variables.get()`, anything sensitive becomes `defineSecret` + `secrets.getSecret()`.
435
- 5. **Add `pikku.config.json`** at project root with `srcDirectories`, `outDir`, and `clientFiles` — plus `locale` if the team does not work in English, which is the language every `description`, `title` and step `template` is then authored in.
439
+ 5. **Add `pikku.config.json`** at project root with `srcDirectories`, `outDir`, and `clientFiles` — plus `metaLocale` if the team does not work in English, which is the language every `description`, `title` and step `template` is then authored in.
436
440
  6. **Add `pikkufabric.config.json`** at project root with `projectId`, `production.domain`, and `frontends` (production is always `main`, so there is no `production.branch`).
437
441
  7. **Run `pikku all`** — verify codegen succeeds and there are no type errors.
438
442
  8. **Run `pikku fabric validate`** once more to confirm no structural issues remain.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: pikku-feature
3
3
  description: 'Drive create-a-feature work inside a Pikku project that already exists: discover project context, work on a feature branch, implement + verify + commit, and ask the user to review via the diff. TRIGGER when: the user asks to "create a feature", "add X to my Pikku project", "wire up a new endpoint", or anything that implies turning a natural-language request into Pikku functions/wirings/migrations within a working app. DO NOT TRIGGER when: the user asks for a one-off code edit in an existing function, asks about Pikku concepts (use pikku-concepts), or is building a whole app from a fresh scaffold rather than extending one (use pikku-build-app, or pikku-build-quick / pikku-build-platform).'
4
- allowed-tools: Bash(yarn pikku meta *), Bash(yarn pikku all *), Bash(yarn tsc), Bash(git status *), Bash(git diff *), Bash(git switch *), Bash(git checkout *), Bash(git checkout -b *), Bash(git add *), Bash(git commit *), Bash(git log *), Bash(git branch *)
4
+ allowed-tools: Bash(yarn pikku meta *), Bash(yarn pikku all *), Bash(yarn tsc), Bash(git status *), Bash(git diff *), Bash(git switch *), Bash(git checkout *), Bash(git checkout -b *), Bash(git add *), Bash(git commit *), Bash(git log *), Bash(git branch *), Bash(yarn pikku fabric report *), Bash(npx --no pikku fabric report *)
5
5
  argument-hint: '<feature description>'
6
6
  ---
7
7
 
@@ -16,6 +16,7 @@ Use this skill as an execution checklist, not reference material.
16
16
  3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
17
17
  4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
18
18
  5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
19
+ 6. Report anything about pikku itself that cost you time, the moment it happens — see **Report what fought you**.
19
20
 
20
21
  End-to-end flow: **discover → state intent → branch → implement → verify → commit → hand to reviewer**.
21
22
 
@@ -41,7 +42,7 @@ schemas (`yarn pikku meta functions get <id>`) or workflow steps
41
42
  **Capability rule:** do not introduce new wires of a type whose
42
43
  `capabilities.<type>` is `false` unless the user explicitly asked for it.
43
44
 
44
- **Language rule:** read `locale` in `pikku.config.json` (default `en`). It is
45
+ **Language rule:** read `metaLocale` in `pikku.config.json` (default `en`). It is
45
46
  the language of every `description`, `title` and step `template` you author —
46
47
  the meta the Pikku Console renders back to the team. It renames nothing:
47
48
  functions, components, types, files, tables and columns are English in every
@@ -246,6 +247,106 @@ feature/<slug>` and surface the PR-create URL.
246
247
 
247
248
  Do not push without explicit confirmation. Do not merge.
248
249
 
250
+ ## Report what fought you
251
+
252
+ When pikku itself is what cost you time, report it with `pikku fabric report`.
253
+ Nothing is written to the repo; the finding goes to the linked fabric project
254
+ and the terminal shows you exactly what was sent.
255
+
256
+ **Report at the moment it happens**, not at the end from memory — a run that
257
+ falls over never reaches its end. One finding per thing that fought you.
258
+
259
+ ### The ladder
260
+
261
+ 1. **Find the quicker workaround.** The user is paying for their feature, not
262
+ for pikku's health.
263
+ 2. **Investigate** only when there is no workaround, or when the user asks why
264
+ something is slow or wrong.
265
+ 3. **Report at the depth you already reached.** Never spend extra effort to
266
+ file; never throw away effort you already spent. If the investigation took
267
+ you to the mechanism, the finding says so — named file, named function, what
268
+ is actually happening, and what pikku should do instead.
269
+
270
+ **Never fix pikku itself.** Not a patch in `node_modules`, not a linked
271
+ checkout, not a branch in the framework repo. Many agents each patching pikku to
272
+ unblock themselves is many divergent copies and a merge problem nobody signed up
273
+ for. Work around it in the app, report it, and let the fix happen once.
274
+
275
+ ### What counts
276
+
277
+ Anything that cost you time and would cost the next person the same. Most of
278
+ these never produce an error: output that is quietly wrong, a generated type
279
+ that disagrees with the runtime, a check that passes when it should not, a
280
+ narrowing you had to write by hand because the framework should have written it.
281
+ **Having to write code the framework should have written for you is a finding.**
282
+
283
+ So is anything that only shows up in one place — invisible locally, fatal
284
+ deployed, or the reverse. Say which, with `--surface`.
285
+
286
+ Not a finding: a preference, a thing you would have designed differently, or
287
+ baseline noise that was already failing before you started.
288
+
289
+ ### Two kinds
290
+
291
+ - `--kind product` — pikku behaved wrongly. Fixing it is a change to the
292
+ framework.
293
+ - `--kind harness` — a skill misled you: it told you to run something that does
294
+ not exist, described a flag that is spelled differently, or contradicted what
295
+ the CLI actually did. Pass `--skill <name>` and `--passage "<the line or
296
+ section>"`. This is the most useful kind to file, because it is fixable
297
+ immediately — so file it even when the cost was small.
298
+
299
+ ### When there was no workaround
300
+
301
+ Report it anyway with `--unresolved`, and put what you tried and how each
302
+ attempt failed in `--tried`. That is what stops the next person walking the same
303
+ dead ends. Tell the user what you did instead — abandoned it, shipped something
304
+ degraded, or stopped.
305
+
306
+ `--unresolved` means **no workaround was found**. It does not mean the
307
+ workaround was unpleasant.
308
+
309
+ ### The command
310
+
311
+ Send it as JSON on stdin. Most of a finding is prose, and prose carries
312
+ apostrophes, quotes, backticks and newlines — each one a shell metacharacter
313
+ before it is a character in your sentence. A stack trace passed to `--error`
314
+ breaks the command at its first newline; a backtick in `--actual` runs whatever
315
+ follows it. Quote the heredoc delimiter (`<<'EOF'`, never `<<EOF`) so the shell
316
+ leaves the body alone.
317
+
318
+ ```bash
319
+ pikku fabric report --stdin <<'EOF'
320
+ {
321
+ "title": "<one-line title>",
322
+ "kind": "product",
323
+ "model": "<the model you are>",
324
+ "expected": "<what you expected pikku to do>",
325
+ "actual": "<what it did instead>",
326
+ "command": "<the command you ran>",
327
+ "workaround": "<what you did instead, inside the app>"
328
+ }
329
+ EOF
330
+ ```
331
+
332
+ Add whichever of these you actually have: `error` (the error's message line,
333
+ verbatim), `repro` (the shortest way to reach it again), `proposal` (what pikku
334
+ should do), `area`, `surface` (`local`, `deployed` or `both`), `cost` (measured
335
+ if you measured it — "98s vs 20s steady" ranks; "slow" does not), `run` (an id
336
+ shared by every finding from this build), `deployTarget`.
337
+
338
+ The same fields exist as flags — `--kind`, `--expected` and so on — for a
339
+ finding short enough to type. Anything with a newline or a quote in it goes
340
+ through `--stdin`.
341
+
342
+ Versions, platform and package manager are read off the installed tree for you.
343
+ Do not pass them and do not ask the user for them.
344
+
345
+ Reporting never fails a build. A finding that cannot be sent — logged out, or
346
+ fabric unreachable — is held on the machine and goes out with the next report
347
+ that succeeds, so nothing you file is lost. If it says the finding was queued,
348
+ carry on with the feature; do not try to fix it, and do not file it again.
349
+
249
350
  ## Hard constraints
250
351
 
251
352
  The skill's `allowed-tools` does **not** permit:
@@ -254,7 +355,8 @@ The skill's `allowed-tools` does **not** permit:
254
355
  - `yarn dbmigrate` (never run migrations against the real DB during planning)
255
356
  - `pikku deploy apply` (never deploy)
256
357
  - secret writes
257
- - network calls beyond what the implementation requires
358
+ - network calls beyond what the implementation requires, except
359
+ `pikku fabric report`, which is explicitly permitted
258
360
 
259
361
  If the feature genuinely needs any of these, **stop and ask** with a clear
260
362
  explanation of why and what would change.
@@ -25,7 +25,7 @@ this skill. Three axes:
25
25
  | Axis | What it covers | What sets it |
26
26
  | --------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
27
27
  | **Identifiers** | Function, component, type and file names; database tables and columns | Nothing — always English, no setting |
28
- | **Meta** | `description` / `name` / `title` / `template` authored inside the code, which the Pikku Console renders | `locale` in `pikku.config.json`, default `en` |
28
+ | **Meta** | `description` / `name` / `title` / `template` authored inside the code, which the Pikku Console renders | `metaLocale` in `pikku.config.json`, default `en` |
29
29
  | **Product UI** | Every string the app shows a user | `messages/<locale>.json` + `defaultLocale` — **this axis only** |
30
30
 
31
31
  A non-English product moves the third row and nothing else.
@@ -57,7 +57,7 @@ So a German medical portal is **three** settings, not one:
57
57
  { "defaultLocale": "de" }
58
58
 
59
59
  // pikku.config.json — the language the team reads their Console in
60
- { "locale": "de" }
60
+ { "metaLocale": "de" }
61
61
  ```
62
62
 
63
63
  In the Fabric template both of the first two have a command, so you rarely edit
@@ -240,11 +240,11 @@ their own sandbox.
240
240
  import { useDevActors } from '@pikku/react'
241
241
 
242
242
  const { actors, signInAs, pendingEmail, isPending, error } = useDevActors({
243
- // Gate both reads on the bundler's dev flag so the secret cannot reach a
243
+ // Gate both reads on the bundler's dev flag so no credential can reach a
244
244
  // production bundle. The sandbox dev server bakes them from your personas.
245
245
  actors: import.meta.env.DEV ? import.meta.env.VITE_DEV_ACTORS : undefined,
246
- secret: import.meta.env.DEV
247
- ? import.meta.env.VITE_SCENARIO_ACTOR_SECRET
246
+ secrets: import.meta.env.DEV
247
+ ? import.meta.env.VITE_DEV_ACTOR_SECRETS
248
248
  : undefined,
249
249
  apiUrl: apiUrl(),
250
250
  onSignedIn: () => navigate({ to: '/' }),
@@ -255,8 +255,11 @@ const { actors, signInAs, pendingEmail, isPending, error } = useDevActors({
255
255
  `<DevActorSwitcher />` from `@pikku/mantine/dev` — a separate entry point from
256
256
  `@pikku/mantine/core`, whose contract is "drop-in alias for `@mantine/core`"
257
257
  and so must not export components Mantine has no counterpart for.
258
- - **`actors` is empty unless the host supplied both a list and a secret**, so a
259
- production build renders nothing without you testing for it.
258
+ - **`secrets` is `{ address: credential }`, not one shared value** — a
259
+ credential opens the one persona it was minted for (see
260
+ **pikku-better-auth**). `actors` is empty unless the host supplied both a list
261
+ and the credentials for it, and an actor with no credential is not offered, so
262
+ a production build renders nothing without you testing for it.
260
263
  - **It takes `onSignedIn` rather than a router**, and takes the env values rather
261
264
  than reading them, because how env is spelled is a bundler fact
262
265
  (`import.meta.env.VITE_*` vs `process.env.NEXT_PUBLIC_*`).
@@ -268,6 +271,37 @@ Do not hand-write the `devActors()` / `signInAsActor()` pair per app; that
268
271
  copy-paste, including the `import.meta.env.DEV` gate, is exactly what this
269
272
  replaced.
270
273
 
274
+ ### Linking from a Mantine element: `renderRoot`, not `component`
275
+
276
+ Handing TanStack's `Link` to a Mantine element as `component={Link}` compiles,
277
+ renders, and navigates — and silently unties the type. Mantine's polymorphic
278
+ `component` prop widens the router generic to `AnyRouter`, so `to` and
279
+ `params` stop being checked against your actual routes. Renaming a route then
280
+ breaks the running app instead of the build, which is the one thing the typed
281
+ router exists to prevent.
282
+
283
+ Wrap the typed `Link` once and reach it through `renderRoot`, which passes the
284
+ props through without re-typing the element:
285
+
286
+ ```tsx
287
+ // components/links.tsx — one wrapper the whole app links through
288
+ import { Link } from '@tanstack/react-router'
289
+
290
+ export const AssessmentLink = (props: { assessmentId: string; children: React.ReactNode }) => (
291
+ <Link to="/assessments/$assessmentId" params={{ assessmentId: props.assessmentId }}>
292
+ {props.children}
293
+ </Link>
294
+ )
295
+ ```
296
+
297
+ ```tsx
298
+ <Button renderRoot={(p) => <AssessmentLink assessmentId={id} {...p} />}>
299
+ Open
300
+ </Button>
301
+ ```
302
+
303
+ The wrapper is where `to` and `params` are checked, and it is checked once.
304
+
271
305
  ## What NOT to do
272
306
 
273
307
  - Don't instantiate `PikkuFetch`/`PikkuRPC` inside a component — `createPikku`