@pikku/skills 0.12.20 → 0.12.22

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.20",
3
+ "version": "0.12.22",
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",
@@ -146,7 +146,7 @@ second argument is always present — an addon never falls back to its own logge
146
146
  variables or secrets; the consuming app supplies them:
147
147
 
148
148
  ```typescript
149
- import { pikkuAddonServices } from '#pikku/addon/setup'
149
+ import { pikkuAddonServices } from '#pikku/setup'
150
150
 
151
151
  export const createSingletonServices = pikkuAddonServices(
152
152
  async (config, { secrets, logger }) => {
@@ -167,7 +167,7 @@ config object.
167
167
  Define per-request services for an addon package (created fresh per HTTP request, queue job, etc.):
168
168
 
169
169
  ```typescript
170
- import { pikkuAddonWireServices } from '#pikku/addon/setup'
170
+ import { pikkuAddonWireServices } from '#pikku/setup'
171
171
 
172
172
  export const createWireServices = pikkuAddonWireServices(
173
173
  async (singletonServices, wire) => {
@@ -195,7 +195,7 @@ This generates `package.json` (exports `.pikku/*` + `dist/`), `pikku.config.json
195
195
 
196
196
  ```typescript
197
197
  // src/services.ts
198
- import { pikkuAddonServices, pikkuAddonWireServices } from '#pikku/addon/setup'
198
+ import { pikkuAddonServices, pikkuAddonWireServices } from '#pikku/setup'
199
199
  import { TodoStore } from './todo-store.service.js'
200
200
 
201
201
  export const createSingletonServices = pikkuAddonServices(async () => {
@@ -213,14 +213,15 @@ export const createWireServices = pikkuAddonWireServices(
213
213
 
214
214
  ### Functions
215
215
 
216
- An addon generates its whole tree under `#pikku/addon/*`, so it authors against
217
- `#pikku/addon/function`, `#pikku/addon/http` and so on. An application's leaves
218
- stay flat, which is what stops a linked addon resolving against its host.
216
+ An addon's generated tree roots at `.pikku/addon/`, but its `imports` map points
217
+ `#pikku/*` there, so it authors against the same subpaths an application does —
218
+ `#pikku/function`, `#pikku/http`. The `addon` segment is the package's own
219
+ business, never part of a specifier.
219
220
 
220
221
  ```typescript
221
222
  // src/functions/addTodo.function.ts
222
223
  import { z } from 'zod'
223
- import { pikkuSessionlessFunc } from '#pikku/addon/function'
224
+ import { pikkuSessionlessFunc } from '#pikku/function'
224
225
 
225
226
  const AddTodoInput = z.object({ title: z.string() })
226
227
  const AddTodoOutput = z.object({ id: z.string(), title: z.string() })
@@ -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,9 +474,9 @@ 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
@@ -510,9 +516,9 @@ an `actor: true` row only under `pikku dev`. With the opt-in set, a stage signs
510
516
  in as the personas the deployment provisioned when it started and refuses
511
517
  everything else (`No actor account exists for that address`), so holding the
512
518
  secret on such a stage does not let anyone invent identities. Those rows are
513
- written by `provisionPersonas` from `@pikku/better-auth`, called in the server's
514
- own lifecycle, so provisioning needs no actor secret and works on a stage whose
515
- endpoint is shut.
519
+ written by the fabric plugin when an operator asks to act as an address the
520
+ stage has no account for, so provisioning needs no actor secret and works on a
521
+ stage whose endpoint is shut.
516
522
 
517
523
  **`SCENARIO_ACTOR_SECRET` is a credential as powerful as the most privileged
518
524
  persona.** Provisioning grants declared roles to actor accounts, so an
@@ -538,30 +544,46 @@ This is the endpoint `pikku scenario` signs its actors in through, and the one
538
544
  the frontend dev switcher posts to — see `pikku-scenario` for declaring the
539
545
  actors and `pikku-react` for `useDevActors()`.
540
546
 
541
- ### Provisioning personas (`provisionPersonas`)
547
+ ### Provisioning personas
542
548
 
543
- Anywhere but `pikku dev`, the accounts have to exist before anyone signs in.
544
- The deployment creates them itself, from its own lifecycle:
549
+ Anywhere but `pikku dev`, the accounts have to exist before anyone signs in. The
550
+ stage creates them itself, from the personas you hand `pikkuFabric`:
545
551
 
546
552
  ```ts
547
- import { provisionPersonas } from '@pikku/better-auth'
553
+ import { pikkuFabric } from '@pikku/better-auth'
548
554
  import {
549
555
  personaConfigs,
550
556
  personaEnvironments,
551
557
  } from '#pikku/pikku-personas.gen.js'
552
558
 
553
- await provisionPersonas(singletonServices, {
554
- personas: personaConfigs,
555
- environments: personaEnvironments,
559
+ pikkuFabric({
560
+ publicKey,
561
+ audience,
562
+ scopeService,
563
+ personas: {
564
+ personas: personaConfigs,
565
+ environments: personaEnvironments,
566
+ },
556
567
  })
557
568
  ```
558
569
 
559
- It runs where the database already is, which is the point: the CLI has no
560
- connection to a deployed environment's database — it resolves one from the local
561
- project config — so a `pikku persona sync staging` that wrote rows would write
562
- them to whatever database the checkout happened to point at. `pikku persona sync
563
- <environment>` still exists, and reports who that environment will provision and
564
- why anyone was skipped, which is what you run _before_ the deploy.
570
+ There is nothing else to call and nothing to schedule. The plugin's operator
571
+ endpoint resolves the address the caller wants to act as; a miss provisions the
572
+ declaration and looks again. On a stage that already holds the persona that is
573
+ one query, and the pass only runs when there is genuinely something absent to
574
+ create.
575
+
576
+ **Do not reach for `pikkuServerLifecycle`'s `afterStart` for this.** That hook is
577
+ invoked by `pikku serve` and `pikku dev` and by nothing else — no deploy runtime
578
+ calls it — so a stage on Workers or a serverless target that provisioned from
579
+ `afterStart` provisioned nothing, and every persona signed in holding no roles.
580
+
581
+ Provisioning runs where the database already is, which is the point: the CLI has
582
+ no connection to a deployed environment's database — it resolves one from the
583
+ local project config — so a `pikku persona sync staging` that wrote rows would
584
+ write them to whatever database the checkout happened to point at. `pikku persona
585
+ sync <environment>` still exists, and reports who that environment will provision
586
+ and why anyone was skipped, which is what you run _before_ the deploy.
565
587
 
566
588
  It creates missing accounts as `actor: true`, applies the roles each persona
567
589
  declares, and is additive — it never revokes. `PIKKU_ENV` (or an explicit
@@ -577,15 +599,20 @@ open. By default provisioning warns about those accounts and changes nothing.
577
599
  `orphans: 'ban'` shuts them:
578
600
 
579
601
  ```ts
580
- await provisionPersonas(singletonServices, {
581
- personas: personaConfigs,
582
- environments: personaEnvironments,
583
- orphans: 'ban',
602
+ pikkuFabric({
603
+ publicKey,
604
+ audience,
605
+ scopeService,
606
+ personas: {
607
+ personas: personaConfigs,
608
+ environments: personaEnvironments,
609
+ orphans: 'ban',
610
+ },
584
611
  })
585
612
  ```
586
613
 
587
614
  It writes the same `banned` column the console's ban RPC writes (so it needs the
588
- `ban()` plugin wired, and says so if it isn't), revokes the account's sessions,
615
+ `pikkuBan()` plugin wired, and says so if it isn't), revokes the account's sessions,
589
616
  and leaves the row, its grants and its history intact — provisioning lifts the
590
617
  ban again by itself if the persona comes back. Deleting is deliberately not
591
618
  offered: an actor row is referenced by whatever those scenarios did while it
@@ -210,7 +210,7 @@ people the user named, and the roles they imply.
210
210
 
211
211
  ```typescript
212
212
  import { definePersonas } from '#pikku/scopes/pikku-personas.gen.js'
213
- import { defineSystemRole } from '#pikku'
213
+ import { defineSystemRole } from '#pikku/scopes'
214
214
 
215
215
  defineSystemRole({
216
216
  owner: {
@@ -484,7 +484,7 @@ experiences it. Three ship in `packages/functions/test/scenarios/` — keep them
484
484
  green — and every milestone's gherkin block from §5 becomes one more.
485
485
 
486
486
  ```typescript
487
- import { pikkuScenario } from '#pikku/workflow/pikku-workflow-types.gen.js'
487
+ import { pikkuScenario } from '#pikku/scenarios'
488
488
 
489
489
  export const tenantReportsAFaultScenario = pikkuScenario<void, { id: string }>({
490
490
  title: 'A tenant reports a fault and the owner sees it',
@@ -85,7 +85,7 @@ named:
85
85
 
86
86
  ```typescript
87
87
  import { definePersonas } from '#pikku/scopes/pikku-personas.gen.js'
88
- import { defineSystemRole } from '#pikku'
88
+ import { defineSystemRole } from '#pikku/scopes'
89
89
 
90
90
  defineSystemRole({
91
91
  owner: { displayName: 'Owner', description: 'Sees only their own rows', scopes: [] },
@@ -199,7 +199,7 @@ Not the full ladder — one journey, end to end, as a real persona, so the app h
199
199
  at least one thing that stays true.
200
200
 
201
201
  ```typescript
202
- import { pikkuScenario } from '#pikku/workflow/pikku-workflow-types.gen.js'
202
+ import { pikkuScenario } from '#pikku/scenarios'
203
203
 
204
204
  export const ownerCreatesAndSeesItScenario = pikkuScenario<void, { id: string }>({
205
205
  title: 'An owner creates a thing and sees it',
@@ -247,6 +247,8 @@ export const lifecycle = pikkuServerLifecycle<SingletonServices>({
247
247
 
248
248
  Export exactly one `pikkuServerLifecycle` from anywhere in `srcDirectories` — the inspector finds it by the wrapper call. Every hook is optional and receives the already-created singleton services. See pikku-services for the ordering and the `afterStop` caveat.
249
249
 
250
+ **Only `pikku dev` and `pikku serve` invoke these hooks.** No deploy runtime does, so anything a Workers or serverless stage needs done cannot live here — put it on the request path that needs it, guarded by a cheap check.
251
+
250
252
  **2. Bootstrap it yourself (required for a specific runtime)**
251
253
 
252
254
  Express, Fastify, uWS, Lambda, Cloudflare and Next.js need their own entrypoint, because Pikku is embedded in a server you own:
@@ -333,7 +335,7 @@ bottom.
333
335
  | Axis | What it covers | What decides it |
334
336
  | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
335
337
  | **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. | `metaLocale` in `pikku.config.json`. Defaults to `en`. |
338
+ | **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
339
  | **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
340
 
339
341
  ### Identifiers are English, and nothing changes that
@@ -119,7 +119,7 @@ without it in production?"** If yes, it is configuration and belongs in a
119
119
  migration, however much it looks like sample data. A venue and its rooms, a
120
120
  product catalogue, a tenant, a country list, the organization the whole
121
121
  deployment hangs off — all configuration. Accounts and role grants are
122
- provisioning: `provisionPersonas` at deployment, or a migration. What is left
122
+ provisioning: the fabric plugin's `personas`, or a migration. What is left
123
123
  over is the seed's job — the bookings, orders and messages a demo needs and a real
124
124
  environment starts without.
125
125
 
@@ -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
 
@@ -306,13 +306,13 @@ tells you nothing about whether it worked. `--sync` waits for a terminal state
306
306
  and exits non-zero unless the deployment went live, which is the only form worth
307
307
  running in CI:
308
308
 
309
- | exit | meaning |
310
- | ---- | ------- |
311
- | 0 | live (or queued, without `--sync`) |
312
- | 1 | the command could not run — not logged in, unsafe git state, bad flags |
313
- | 2 | the deployment failed, errored, timed out server-side, or was cancelled |
314
- | 3 | the deployment is blocked and nothing the CLI can do will unblock it |
315
- | 4 | the wait hit `--timeout` with the deployment still in flight |
309
+ | exit | meaning |
310
+ | ---- | ----------------------------------------------------------------------- |
311
+ | 0 | live (or queued, without `--sync`) |
312
+ | 1 | the command could not run — not logged in, unsafe git state, bad flags |
313
+ | 2 | the deployment failed, errored, timed out server-side, or was cancelled |
314
+ | 3 | the deployment is blocked and nothing the CLI can do will unblock it |
315
+ | 4 | the wait hit `--timeout` with the deployment still in flight |
316
316
 
317
317
  Fabric parks every deploy at a gate after the plan phase (`status: suspended`).
318
318
  Why it parked is the whole story, and it is `statusReason`, not `status`:
@@ -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
 
@@ -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.
@@ -226,7 +226,7 @@ export const getBook = pikkuFunc({
226
226
  })
227
227
 
228
228
  // wirings/books.http.ts — same defineHTTPRoutes/wireHTTPRoutes shape as the Route Groups example above
229
- import { addHTTPMiddleware } from '#pikku/http'
229
+ import { addHTTPMiddleware } from '#pikku/middleware'
230
230
  import { cors, authBearer } from '@pikku/core/middleware'
231
231
 
232
232
  addHTTPMiddleware('*', [cors(), authBearer()])
@@ -23,7 +23,7 @@ installGroups: [core]
23
23
  ## The `pikkuMiddleware` Factory
24
24
 
25
25
  ```typescript
26
- import { pikkuMiddleware } from '#pikku/function'
26
+ import { pikkuMiddleware } from '#pikku/middleware'
27
27
 
28
28
  // Simple: just a function
29
29
  const myMiddleware = pikkuMiddleware(async (services, wire, next) => {
@@ -139,7 +139,7 @@ Tags from the function definition and the wire object are merged — middleware
139
139
  ### Registering Tag Middleware
140
140
 
141
141
  ```typescript
142
- import { addTagMiddleware } from '#pikku/function'
142
+ import { addTagMiddleware } from '#pikku/middleware'
143
143
 
144
144
  addTagMiddleware('machine-agent', [machineAgentBearerAuth])
145
145
  ```
@@ -277,7 +277,7 @@ export const getToken = () => _token
277
277
  ```typescript
278
278
  // wirings/http.wiring.ts
279
279
  import { timingSafeEqual } from 'node:crypto'
280
- import { addTagMiddleware, pikkuMiddleware } from '#pikku/function'
280
+ import { addTagMiddleware, pikkuMiddleware } from '#pikku/middleware'
281
281
  import { UnauthorizedError } from '#pikku/error'
282
282
  import { getToken } from '../lib/host-token.js'
283
283
 
@@ -58,7 +58,7 @@ Use for checks that read the session but need no request data — and that asser
58
58
  something **beyond** merely having a session (a flag, a tier, a claim).
59
59
 
60
60
  ```typescript
61
- import { pikkuAuth } from '#pikku/function'
61
+ import { pikkuAuth } from '#pikku/auth'
62
62
 
63
63
  // Good: a real gate on the session's contents, not just its existence.
64
64
  export const isVerified = pikkuAuth(
@@ -89,7 +89,7 @@ A permission answers "_may this user do this?_" (role, ownership, tier) — neve
89
89
  Use when authorization depends on the actual request data (e.g., resource ownership).
90
90
 
91
91
  ```typescript
92
- import { pikkuPermission } from '#pikku/function'
92
+ import { pikkuPermission } from '#pikku/auth'
93
93
 
94
94
  export const isBookOwner = pikkuPermission(
95
95
  async ({ db }, { bookId }, { session }) => {
@@ -139,7 +139,7 @@ export const deleteBook = pikkuFunc({
139
139
  A global permission is an app-wide baseline that **every** function must additionally pass. It is an independent AND gate: it can only ever _narrow_ access — it never grants access a function's own `permissions` would deny.
140
140
 
141
141
  ```typescript
142
- import { addGlobalPermission } from '#pikku/function'
142
+ import { addGlobalPermission } from '#pikku/auth'
143
143
 
144
144
  addGlobalPermission([isEmployee]) // every function now also requires an employee session
145
145
  ```
@@ -248,7 +248,7 @@ declared, inspectable, and reusable.
248
248
 
249
249
  ```typescript
250
250
  // src/permissions.ts
251
- import { pikkuAuth, pikkuPermission } from '#pikku/function'
251
+ import { pikkuAuth, pikkuPermission } from '#pikku/auth'
252
252
 
253
253
  export const isVerified = pikkuAuth(
254
254
  async (_services, session) => !!session?.emailVerified
@@ -46,7 +46,7 @@ Scenarios live in `srcDirectories` like any other function — by convention `*.
46
46
  `pikkuScenario` comes from the **generated** workflow types, not `@pikku/core`:
47
47
 
48
48
  ```typescript
49
- import { pikkuScenario } from '#pikku/scenario'
49
+ import { pikkuScenario } from '#pikku/scenarios'
50
50
 
51
51
  export const orderSupportScenario = pikkuScenario<
52
52
  { value?: number },
@@ -175,7 +175,7 @@ Hooks are scenario-only. A `before`/`after` on a `pikkuWorkflowFunc` never runs
175
175
  `pikkuFeature` groups scenarios the way gherkin's `Feature:` groups `Scenario:`. Scenarios are referenced by **imported identifier**, so a renamed or deleted scenario is a compile error rather than a silent skip:
176
176
 
177
177
  ```typescript
178
- import { pikkuFeature } from '#pikku/scenario'
178
+ import { pikkuFeature } from '#pikku/scenarios'
179
179
  import {
180
180
  credentialLazyLoadScenario,
181
181
  credentialRoundTripScenario,
@@ -240,7 +240,7 @@ Utilities are **not steps**. They are plain exported functions, they take the br
240
240
 
241
241
  ```typescript
242
242
  // shop.browser.ts — shared actions. Not steps: nothing here is an intent.
243
- import type { PikkuBrowserWire } from '#pikku/scenario'
243
+ import type { PikkuBrowserWire } from '#pikku/scenarios'
244
244
  import type {} from '@pikku/playwright'
245
245
 
246
246
  /** Arrive on the shop, from wherever the browser happens to be. */
@@ -462,7 +462,7 @@ Assertions with no possible browser witness are a different thing and should not
462
462
  `scenario.do` can only name an RPC. A **step** is a named, typed unit of scenario behaviour whose body is an ordinary pikku function — so it can call several RPCs as its actor, assert, or drive a browser.
463
463
 
464
464
  ```typescript
465
- import { pikkuScenarioStep } from '#pikku/scenario'
465
+ import { pikkuScenarioStep } from '#pikku/scenarios'
466
466
 
467
467
  export const buysAnApple = pikkuScenarioStep<
468
468
  { qty: number },
@@ -62,7 +62,7 @@ Apply these via `addHTTPMiddleware` in a wirings file:
62
62
 
63
63
  ```typescript
64
64
  import { authBearer, authCookie, authAPIKey } from '#pikku/middleware'
65
- import { addHTTPMiddleware } from '#pikku/http'
65
+ import { addHTTPMiddleware } from '#pikku/middleware'
66
66
 
67
67
  // JWT bearer token — reads Authorization header
68
68
  addHTTPMiddleware('*', [authBearer()])
@@ -118,7 +118,7 @@ response.
118
118
 
119
119
  ```typescript
120
120
  // permissions.ts
121
- import { pikkuAuth, pikkuPermission } from '#pikku/function'
121
+ import { pikkuAuth, pikkuPermission } from '#pikku/auth'
122
122
 
123
123
  export const isAuthenticated = pikkuAuth(
124
124
  async (_services, session) => !!session
@@ -129,7 +129,7 @@ export const isVerified = pikkuAuth(
129
129
 
130
130
  // wirings/auth.wiring.ts
131
131
  import { authCookie } from '#pikku/middleware'
132
- import { addHTTPMiddleware } from '#pikku/http'
132
+ import { addHTTPMiddleware } from '#pikku/middleware'
133
133
 
134
134
  addHTTPMiddleware('*', [
135
135
  authCookie({
@@ -38,7 +38,7 @@ pikku info tags --verbose # Understand project organization
38
38
  ### `pikkuServices(factory)` — singleton services (created once at startup)
39
39
 
40
40
  ```typescript
41
- import { pikkuServices } from '#pikku/function'
41
+ import { pikkuServices } from '#pikku/setup'
42
42
  import { ConsoleLogger } from '@pikku/core/services'
43
43
  import { JoseJWTService } from '@pikku/jose'
44
44
 
@@ -61,7 +61,7 @@ export const createSingletonServices = pikkuServices(
61
61
  ### `pikkuWireServices(factory)` — per-request services (fresh per HTTP request, queue job, CLI command, etc.)
62
62
 
63
63
  ```typescript
64
- import { pikkuWireServices } from '#pikku/function'
64
+ import { pikkuWireServices } from '#pikku/setup'
65
65
 
66
66
  export const createWireServices = pikkuWireServices(
67
67
  async (singletonServices, wire) => {
@@ -240,7 +240,7 @@ const createSingletonServices = pikkuServices(async (config) => {
240
240
 
241
241
  ```typescript
242
242
  // services.ts
243
- import { pikkuServices, pikkuWireServices } from '#pikku/function'
243
+ import { pikkuServices, pikkuWireServices } from '#pikku/setup'
244
244
  import { ConsoleLogger } from '@pikku/core/services'
245
245
  import { JoseJWTService } from '@pikku/jose'
246
246