@pikku/skills 0.12.22 → 0.12.26

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 (106) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +1 -1
  4. package/skills/pikku-a11y/SKILL.md +59 -0
  5. package/skills/pikku-addon/SKILL.md +2 -2
  6. package/skills/pikku-agent/SKILL.md +67 -316
  7. package/skills/pikku-agent/references/agents.md +299 -0
  8. package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
  9. package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
  10. package/skills/pikku-architect/SKILL.md +265 -0
  11. package/skills/pikku-auth/SKILL.md +89 -0
  12. package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +126 -34
  13. package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
  14. package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
  15. package/skills/pikku-auth/references/permissions.md +261 -0
  16. package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +0 -20
  17. package/skills/pikku-build/SKILL.md +88 -0
  18. package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +76 -24
  19. package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
  20. package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +56 -1
  21. package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
  22. package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
  23. package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +4 -20
  24. package/skills/{pikku-build-app → pikku-build}/references/ship.md +9 -3
  25. package/skills/pikku-concepts/SKILL.md +72 -7
  26. package/skills/pikku-concepts/references/concept-mapping.md +8 -8
  27. package/skills/pikku-deploy/SKILL.md +158 -0
  28. package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
  29. package/skills/pikku-deploy/references/cloudflare.md +104 -0
  30. package/skills/pikku-deploy/references/express.md +92 -0
  31. package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
  32. package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
  33. package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
  34. package/skills/pikku-deploy/references/uws.md +72 -0
  35. package/skills/pikku-deploy/references/ws.md +75 -0
  36. package/skills/pikku-emails/SKILL.md +3 -2
  37. package/skills/pikku-fabric/SKILL.md +47 -20
  38. package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
  39. package/skills/pikku-i18n/SKILL.md +62 -207
  40. package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
  41. package/skills/pikku-i18n/references/messages.md +218 -0
  42. package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
  43. package/skills/pikku-knowledge/SKILL.md +15 -0
  44. package/skills/pikku-kysely/SKILL.md +13 -13
  45. package/skills/pikku-list-query/SKILL.md +163 -0
  46. package/skills/pikku-meta/SKILL.md +58 -130
  47. package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
  48. package/skills/pikku-meta/references/meta.md +114 -0
  49. package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
  50. package/skills/pikku-middleware/SKILL.md +5 -5
  51. package/skills/pikku-n8n-import/SKILL.md +0 -1
  52. package/skills/pikku-permissions/SKILL.md +75 -229
  53. package/skills/pikku-react/SKILL.md +50 -298
  54. package/skills/pikku-react/references/client.md +313 -0
  55. package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
  56. package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
  57. package/skills/pikku-realtime/SKILL.md +110 -251
  58. package/skills/pikku-scenario/SKILL.md +60 -45
  59. package/skills/pikku-scenario/references/persona-run.md +148 -0
  60. package/skills/pikku-seo/SKILL.md +133 -0
  61. package/skills/pikku-service-backends/SKILL.md +154 -0
  62. package/skills/pikku-service-backends/references/aws.md +106 -0
  63. package/skills/pikku-service-backends/references/backblaze.md +57 -0
  64. package/skills/pikku-service-backends/references/mongodb.md +90 -0
  65. package/skills/pikku-service-backends/references/redis.md +75 -0
  66. package/skills/pikku-service-backends/references/schema.md +63 -0
  67. package/skills/pikku-services/SKILL.md +68 -291
  68. package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
  69. package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
  70. package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
  71. package/skills/pikku-services/references/services.md +272 -0
  72. package/skills/pikku-software-archaeology/README.md +5 -1
  73. package/skills/pikku-software-archaeology/SKILL.md +16 -2
  74. package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
  75. package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
  76. package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
  77. package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
  78. package/skills/pikku-webhook/SKILL.md +224 -0
  79. package/skills/pikku-wiring/SKILL.md +180 -0
  80. package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
  81. package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
  82. package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
  83. package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +3 -39
  84. package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
  85. package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
  86. package/skills/pikku-wiring/references/realtime.md +265 -0
  87. package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
  88. package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
  89. package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
  90. package/skills/pikku-workflow/SKILL.md +39 -2
  91. package/skills/pikku-aws/SKILL.md +0 -161
  92. package/skills/pikku-backblaze/SKILL.md +0 -104
  93. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
  94. package/skills/pikku-deploy-express/SKILL.md +0 -122
  95. package/skills/pikku-deploy-uws/SKILL.md +0 -144
  96. package/skills/pikku-mongodb/SKILL.md +0 -113
  97. package/skills/pikku-product-second-opinion/README.md +0 -43
  98. package/skills/pikku-redis/SKILL.md +0 -99
  99. package/skills/pikku-schema-ajv/SKILL.md +0 -83
  100. package/skills/pikku-schema-cfworker/SKILL.md +0 -82
  101. package/skills/pikku-ws/SKILL.md +0 -87
  102. /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
  103. /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
  104. /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
  105. /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
  106. /package/skills/{pikku-realtime/references/other-routes.md → pikku-wiring/references/realtime-other-routes.md} +0 -0
@@ -1,17 +1,18 @@
1
1
  ---
2
2
  name: pikku-scenario
3
3
  description: >-
4
- Use when writing or running Pikku scenarios, or when asked to test Pikku functions or improve
5
- test coverage. A scenario (pikkuScenario) drives the app the way users do — steps run as actors
6
- over the real transport against a running server — so a flow doubles as an e2e test and a
7
- staged/production health check. Covers scenario.do / expectEventually / expectError /
8
- expectService / expectScore, declared steps via pikkuScenarioStep (including browser steps driven by
9
- @pikku/playwright) written as intent rather than as clicks, with the actions factored into
10
- shared browser utilities, actors and environments in pikku.config.json, SCENARIO_ACTOR_SECRET, the
11
- `pikku scenario list|run` commands, live function coverage via `pikku dev --coverage`, and
12
- plain unit tests for pure function logic. TRIGGER when: user asks about scenarios, testing a
13
- Pikku function, test coverage, end-to-end flows, browser/UI e2e, or health checks. DO NOT
14
- TRIGGER when: user asks about running an existing test suite (use Bash) or CI configuration.
4
+ Use when writing or running Pikku scenarios, running a persona as a virtual user, or when
5
+ asked to test Pikku functions or improve coverage. A scenario (pikkuScenario) drives the app
6
+ the way users do — steps run as actors over the real transport against a running server — so a
7
+ flow doubles as an e2e test and a staged/production health check. Covers scenario.do /
8
+ expectEventually / expectError / expectService / expectScore, declared steps via
9
+ pikkuScenarioStep (browser steps driven by @pikku/playwright) written as intent rather than
10
+ clicks, personas / actors / environments in pikku.config.json, SCENARIO_ACTOR_SECRET, the
11
+ `pikku scenario list|run` and `pikku persona run|list|sync|secret` commands, and live coverage
12
+ via `pikku dev --coverage`. TRIGGER when: user asks about scenarios, testing a Pikku function,
13
+ coverage, e2e flows, browser/UI e2e, health checks, personas, virtual users, or adversarial
14
+ runs against a stage. DO NOT TRIGGER when: user asks about running an existing suite (use
15
+ Bash) or CI config.
15
16
  installGroups: [core]
16
17
  ---
17
18
 
@@ -29,6 +30,13 @@ Use this skill as an execution checklist, not reference material.
29
30
 
30
31
  **`pikku tests` does not exist.** It was removed in #865 — scenarios own coverage now. Any reference you find to it is stale.
31
32
 
33
+ ## Pick the reference
34
+
35
+ | You are… | Read |
36
+ | ---------------------------------------------------------------- | --------------------------- |
37
+ | Writing or running scenarios | this skill |
38
+ | Running a persona as a model-driven virtual user against a stage | `references/persona-run.md` |
39
+
32
40
  ## What a scenario is
33
41
 
34
42
  A scenario is a `pikkuScenario` export that drives the app **as real actors over the real transport**, against a running server. That is what lets one artifact serve as both an e2e test and a staged/production health check.
@@ -379,10 +387,13 @@ identifier is an API, and it is read by the toolchain.
379
387
 
380
388
  ```typescript
381
389
  // pikku.config.json: { "metaLocale": "de" }
382
- export const buysAnApple = pikkuScenarioStep<{ qty: number }, { orderId: string }>({
383
- name: 'buysAnApple', // identifier — English, always
384
- description: 'kauft einen Apfel', // prose — follows locale
385
- template: 'kauft {qty} Äpfel', // prose — follows locale
390
+ export const buysAnApple = pikkuScenarioStep<
391
+ { qty: number },
392
+ { orderId: string }
393
+ >({
394
+ name: 'buysAnApple', // identifier — English, always
395
+ description: 'kauft einen Apfel', // prose — follows locale
396
+ template: 'kauft {qty} Äpfel', // prose — follows locale
386
397
  actor: true,
387
398
  default: async (_services, { qty }, { actor }) =>
388
399
  await actor.invoke('placeOrder', { qty }),
@@ -396,11 +407,10 @@ A product with a non-English UI is not on its own a reason to set `metaLocale`
396
407
  is the app's language, not the team's. Ask, or leave it `en`.
397
408
 
398
409
  **Where a non-`en` `metaLocale` still shows English, today.** The reporter composes a
399
- sentence as `<Keyword> the <actor> <template>` (`composeStepProse`), and both the
400
- keyword and the article `the` are English literals. The Console translates the
401
- Given/When/Then keywords into its own UI language; the CLI reporter does not, and
402
- nothing translates `the`. So `metaLocale: "de"` gives you German step prose inside an
403
- English frame — `Given the shopper kauft 1 Äpfel`. Write templates that read
410
+ sentence as `<Keyword> <actor> <template>` (`composeStepProse`), and the keyword is
411
+ an English literal. The Console translates the Given/When/Then keywords into its own
412
+ UI language; the CLI reporter does not, so `metaLocale: "de"` gives you German step
413
+ prose inside an English frame — `Given shopper kauft 1 Äpfel`. Write templates that read
404
414
  acceptably in that frame rather than trying to defeat it. A second gap: where a
405
415
  function or scenario declares no `title`, the Console falls back to splitting the
406
416
  **identifier** into an English-looking label (`toEnglishName`), so under a
@@ -502,6 +512,7 @@ Rules that bite:
502
512
  - **Steps default to `retries: 0`**, unlike ordinary workflow steps. Retrying a failed assertion is wrong; pass `retries` explicitly if a step is genuinely flaky-by-nature.
503
513
  - **Step results are persisted**, so return JSON-serialisable data — never a `Locator` or a client object.
504
514
  - **`description` documents the step; `template` is what the report renders.** `template`'s `{placeholders}` are filled from the input the step was called with, so one step reads differently for each call — `sees {state} addon {packageName}` reports as "sees available addon @pikku/addon-stripe". Reflect every input field in the template, and type the values so they read as words (`state?: 'installed' | 'available'`, not `installed?: boolean`). A placeholder with no value renders as nothing and the whitespace collapses.
515
+ - **Never write the actor into the prose.** The reporter renders the actor as the sentence's subject, so a step authored as `` `'sam' creates the client` `` run as `{ actor: actors.sam }` reads "Given sam 'sam' creates the client" — and the hardcoded name desyncs the moment the call site changes actor. Write a bare third-person predicate (`creates the {name} client`) and let the actor supply the subject. Prose that opens with its own actor's key — quoted, capitalised or possessive — is `PKU681`; naming someone **else** mid-sentence ("sends nadia an invite") is ordinary prose and is left alone, as is an actor keyed after a role noun used as a noun ("creates the admin client" as `actors.admin`).
505
516
  - Prose precedence is `options.description` → the step's `template` → the step's own `description` → the positional step name. Repeated names get `#1`, `#2` ordinals, so a `for` loop over a data set is how you write a Scenario Outline. A loop-generated step name is not statically known, so it is matched back to its declaration by step function instead — which works as long as that function's call sites agree on their phase, actor and prose. Two call sites that disagree make the loop step report under its bare runtime name.
506
517
 
507
518
  #### What a step is given
@@ -548,7 +559,7 @@ Two consequences follow, and both shape how steps get written:
548
559
  as an RPC — which usually improves the product, since a client debugging the
549
560
  same problem needed it too.
550
561
  - **`agentRunner` is conditional.** It is built only when the project declares
551
- agents, and `createDevAgentRunner` needs a base URL *and* a key together
562
+ agents, and `createDevAgentRunner` needs a base URL _and_ a key together
552
563
  (`OPENAI_BASE_URL` + `OPENAI_API_KEY`, or the LiteLLM pair). With a key alone
553
564
  it returns nothing and `agentRunner` is `undefined`, so `actor.converse`
554
565
  fails before the persona says anything. A suite that would rather own its own
@@ -596,12 +607,16 @@ import type messages from '../../../../apps/web/messages/en.json'
596
607
 
597
608
  export type MessageKey = keyof typeof messages
598
609
 
599
- export const t = (key: MessageKey, locale = baseLocale): string => { /* … */ }
610
+ export const t = (key: MessageKey, locale = baseLocale): string => {
611
+ /* … */
612
+ }
600
613
  ```
601
614
 
602
615
  ```typescript
603
616
  await page.getByLabel(t('jobs_apply_fullname')).fill(identity.name)
604
- await page.getByRole('button', { name: t('jobs_apply_submit'), exact: true }).click()
617
+ await page
618
+ .getByRole('button', { name: t('jobs_apply_submit'), exact: true })
619
+ .click()
605
620
  ```
606
621
 
607
622
  - Type off `messages/<baseLocale>.json`, **not** the generated Paraglide output — `i18n/paraglide/` is build output, so typing against it makes the tests unbuildable until the app has been built. The JSON is the tracked source.
@@ -676,11 +691,11 @@ Generated files are exempt and never claim the slot.
676
691
  What that admits and what it does not:
677
692
 
678
693
  ```typescript
679
- personality: 'Wound up and short with it.' // read
694
+ personality: 'Wound up and short with it.' // read
680
695
  personality: `Wound up and short with it.
681
- Says what she wants in a few blunt words.` // read — no ${} in it
682
- personality: 'Wound up. ' + 'Short with it.' // dropped, silently
683
- personality: TEMPERAMENTS.impatient // dropped, silently
696
+ Says what she wants in a few blunt words.` // read — no ${} in it
697
+ personality: 'Wound up. ' + 'Short with it.' // dropped, silently
698
+ personality: TEMPERAMENTS.impatient // dropped, silently
684
699
  ```
685
700
 
686
701
  A no-substitution template literal is a string literal as far as the reader is
@@ -704,14 +719,14 @@ An actor with no `persona` is its own persona, so a project that never declares
704
719
  ### The same actors sign a human in
705
720
 
706
721
  Declared actors are not only for automated runs. `signInPath` is Better Auth's
707
- `actor` plugin (see `pikku-better-auth`, a separate install), which any caller can post to — so the
722
+ `actor` plugin (see `pikku-auth`, a separate install), which any caller can post to — so the
708
723
  frontend gets a one-click "Sign in as …" switcher over the **same** list, and an
709
724
  app can be reviewed as each kind of user without anyone knowing a seed password.
710
725
 
711
726
  The sandbox dev server bakes both halves into the frontend from the declared
712
727
  personas: `VITE_DEV_ACTORS` (the JSON actor list) and `VITE_DEV_ACTOR_SECRETS`
713
728
  (`{ email: credential }`, one per persona — `SCENARIO_ACTOR_SECRET` itself never
714
- goes in a bundle; see **pikku-better-auth**). Neither var is set in a production
729
+ goes in a bundle; see **pikku-auth**). Neither var is set in a production
715
730
  build, so the control renders nothing there — but gate the reads on your
716
731
  bundler's dev flag anyway (`import.meta.env.DEV ? … : undefined`) so no
717
732
  credential reaches a production bundle in the first place.
@@ -825,22 +840,22 @@ Services are plain objects — a Pikku function is pure business logic, so a moc
825
840
 
826
841
  ## Red flags
827
842
 
828
- | Smell | Why it's wrong |
829
- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
830
- | `pikku tests …` | Removed in #865. Use `pikku scenario`. |
831
- | `.feature` files / Gherkin for function tests | Scenarios are TypeScript, not Gherkin. The in-process cucumber function world was deleted. |
832
- | `scenario.do(...)` with no `{ actor }` | Throws. Every step runs as somebody. |
833
- | A scenario per function | Scenarios are user flows. One flow covers many functions; that is the point. |
834
- | Assuming a clean database | There is no state reset — it may be a staging server. Scope what you create. |
835
- | `sleep()` before asserting | Use `expectEventually`. |
836
- | A step named `clicksAddToBasket` / `opensThePage` | That is an action, not an intent. Name the step for what the actor wanted; put the clicking in a utility. |
837
- | A step named `kauftEinenApfel` / a `vorgang` table | Identifiers are English in every project. The German belongs in `description` / `template`, and only when `pikku.config.json` sets `metaLocale`. |
838
- | A browser step that assumes it is already on a page | It can then only run mid-flow. Arrive first — check the URL, navigate if needed. |
839
- | `getByLabel('Full Name')` in a translated app | Passes only in the base locale, and a copy edit breaks it as an unexplained timeout. Locate by message key. |
840
- | A `browser` binding guarding `if (!browser)` | The binding guarantees it. The guard hides the real error, which is a missing actor (`PKU677`). |
841
- | A step with a `func:` instead of a surface binding | There is no `func` on a step. Bodies live under `default` / `browser` / `cli`; a step with none throws at load. |
842
- | `expectEventually` in a `pikkuWorkflowFunc` | `PKU675` — scenario-only. |
843
- | Coverage silently 0 | Server not run with `--coverage`, verbose functions meta not deployed, `scaffold.scenarios` unset, or no actors configured. |
843
+ | Smell | Why it's wrong |
844
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
845
+ | `pikku tests …` | Removed in #865. Use `pikku scenario`. |
846
+ | `.feature` files / Gherkin for function tests | Scenarios are TypeScript, not Gherkin. The in-process cucumber function world was deleted. |
847
+ | `scenario.do(...)` with no `{ actor }` | Throws. Every step runs as somebody. |
848
+ | A scenario per function | Scenarios are user flows. One flow covers many functions; that is the point. |
849
+ | Assuming a clean database | There is no state reset — it may be a staging server. Scope what you create. |
850
+ | `sleep()` before asserting | Use `expectEventually`. |
851
+ | A step named `clicksAddToBasket` / `opensThePage` | That is an action, not an intent. Name the step for what the actor wanted; put the clicking in a utility. |
852
+ | A step named `kauftEinenApfel` / a `vorgang` table | Identifiers are English in every project. The German belongs in `description` / `template`, and only when `pikku.config.json` sets `metaLocale`. |
853
+ | A browser step that assumes it is already on a page | It can then only run mid-flow. Arrive first — check the URL, navigate if needed. |
854
+ | `getByLabel('Full Name')` in a translated app | Passes only in the base locale, and a copy edit breaks it as an unexplained timeout. Locate by message key. |
855
+ | A `browser` binding guarding `if (!browser)` | The binding guarantees it. The guard hides the real error, which is a missing actor (`PKU677`). |
856
+ | A step with a `func:` instead of a surface binding | There is no `func` on a step. Bodies live under `default` / `browser` / `cli`; a step with none throws at load. |
857
+ | `expectEventually` in a `pikkuWorkflowFunc` | `PKU675` — scenario-only. |
858
+ | Coverage silently 0 | Server not run with `--coverage`, verbose functions meta not deployed, `scaffold.scenarios` unset, or no actors configured. |
844
859
 
845
860
  `@pikku/cucumber` is a **browser/e2e** harness (`Actor`, `BrowserWorld`, `PersonaData`, `DbUtils`) — out of scope here.
846
861
 
@@ -0,0 +1,148 @@
1
+ # Running a persona as a virtual user
2
+
3
+ `pikku persona run <environment> <persona>` signs a declared persona in over the
4
+ app's real auth and works the API in character, driven by a model. A persona
5
+ while running **is** the virtual user — there is no second declaration for it.
6
+
7
+ **It is not a test runner.** It asserts nothing, and a green run proves nothing:
8
+ what it produces is _findings_, and their absence is only ever "not this time,
9
+ not with this seed". Findings set exit code 1, so a run can gate a pipeline;
10
+ giving up on a goal does not, because that is a user being a user.
11
+
12
+ Everything it needs is already in the project — the catalogue is the function
13
+ meta, the intents are the scenarios' own prose, the identity is the persona
14
+ signing in, the scopes come from their declared roles. The only new input is
15
+ which person to be.
16
+
17
+ Declaring personas — persona versus actor, `definePersonas`, materialised
18
+ actors — is in the skill itself, under **Personas and actors**. This is the
19
+ running half.
20
+
21
+ ## The shape of a run
22
+
23
+ ```bash
24
+ SCENARIO_ACTOR_SECRET=… pikku persona run local shopper
25
+ SCENARIO_ACTOR_SECRET=… pikku persona run local shopper -d careless --seed 42
26
+ SCENARIO_ACTOR_SECRET=… pikku persona run staging auditor \
27
+ --goals "reconcile the order totals" --steps 80 --out runs/auditor.json
28
+ ```
29
+
30
+ Both arguments are required positionals: the environment key from
31
+ `environments`, then the persona id. A run needs a model — `--model`, or
32
+ `scenarios.model` in `pikku.config.json` — and an AI provider in the
33
+ environment (`OPENAI_BASE_URL` + `OPENAI_API_KEY`, or `LITELLM_PROXY_URL` +
34
+ `LITELLM_API_KEY`).
35
+
36
+ | Flag | Effect |
37
+ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
38
+ | `--disposition` / `-d` | How they behave. Overrides the persona's own |
39
+ | `--goals` | Comma-separated, in your words — run _alongside_ the persona's own and the ones derived from scenarios |
40
+ | `--steps` | Model turns before it stops (default 40) |
41
+ | `--mutations` | Non-read calls before it stops |
42
+ | `--duration` | Wall clock before it stops, e.g. `30m` |
43
+ | `--seed` | Replay — the same seed schedules the same run |
44
+ | `--model` | The model they think with |
45
+ | `--allow-approval` | Offer the endpoints the app marked as needing a human's approval. Off by default: those are the ones that spend money |
46
+ | `--skip-role-check` | Start without verifying declared roles against the stage |
47
+ | `--api-url` | Override the environment's `apiUrl`, for a target that only exists at run time. It replaces the url, not the environment's classification — see below |
48
+ | `--out` | Write the whole run — every step, response and finding — as JSON |
49
+
50
+ ## The dispositions
51
+
52
+ A disposition is a bundle of instructions and mechanical dials (move weights,
53
+ temperature, repeat and re-read rates). `tuning` on the persona adjusts those
54
+ dials without replacing the character — a tuned `careless` user is still
55
+ careless. Passing `--disposition` drops the persona's `tuning`, because you
56
+ asked to run them differently rather than to bend their dials into another
57
+ shape.
58
+
59
+ | Disposition | Who that is |
60
+ | ------------- | ----------------------------------------------------------------------------------------------------------------------------- |
61
+ | `realistic` | The default. A competent user reading schemas and entering plausible values |
62
+ | `careless` | Busy, interrupted, half-remembering; submits twice, enters odd-but-legal values. **Where most production bugs actually live** |
63
+ | `newcomer` | First time here, holds no ids in their head, must find a path from the lists that exist (`emptyMemory`) |
64
+ | `stale` | Working from old notes — reaches for ids that may no longer resolve, to see how the product says so |
65
+ | `auditor` | Reconciling, not achieving: reads one fact from every endpoint claiming to know it and reports disagreement. Read-only |
66
+ | `adversarial` | Probing whether the boundaries are enforced. Inverted oracle — a 2xx from something it should not reach is the finding |
67
+ | `accountable` | Doing the job for real. The **only** disposition production accepts |
68
+
69
+ ## Credentials, and which one wins
70
+
71
+ Three variables, checked in this order. None of them belongs in
72
+ `pikku.config.json`.
73
+
74
+ 1. **`FABRIC_OPERATOR_TOKEN`** — what a deployed stage accepts. Asymmetric, and
75
+ it needs no account the target would not otherwise have, so it wins over the
76
+ other two when both are present.
77
+ 2. **`PIKKU_PERSONA_SECRETS`** — `id=secret,id=secret`, already-derived
78
+ per-persona credentials. Hand a run only the personas it should be able to
79
+ be; asking for one outside the list is refused by name rather than falling
80
+ through to the root. Mint them with `pikku persona secret [personas...]` —
81
+ naming none mints all.
82
+ 3. **`SCENARIO_ACTOR_SECRET`** — the root secret, which derives every persona's
83
+ credential and is therefore entitled to all of them. Only `pikku dev` serves
84
+ the endpoint it opens.
85
+
86
+ ## Production is opt-in, twice
87
+
88
+ A persona's `environments` omitted means every configured environment **except**
89
+ those flagged `production: true` — nothing reaches production by being
90
+ forgotten. Naming one requires `disposition: 'accountable'`.
91
+
92
+ That rule is checked twice on purpose: the inspector checks the declaration at
93
+ build time, and sign-in re-checks the **effective** disposition — the persona's
94
+ own, or whatever `--disposition` replaced it with — before the run starts. So
95
+ `--disposition adversarial` cannot point an accountable persona at production.
96
+ The build check trusts the file; the run check does not trust which artifact got
97
+ deployed.
98
+
99
+ **That rule is keyed on the environment's name, not its url.** `production:
100
+ true` is a label a person wrote in `pikku.config.json`; nothing can tell from a
101
+ url whether real customers are behind it. `--api-url` replaces the url and keeps
102
+ the classification, so a non-production environment repointed at a production
103
+ host is still treated as non-production, and an adversarial persona will happily
104
+ run against it. The flag is for a target that only exists at run time — a
105
+ freshly provisioned sandbox. Point it anywhere else and the guard above is not
106
+ protecting you.
107
+
108
+ ## The role check happens before the first step
109
+
110
+ A run reads its own roles back from the stage and compares them to what the
111
+ persona declared. It refuses on a mismatch, before anything runs — findings
112
+ from a persona whose roles drifted are about the seed, and reading them as
113
+ product bugs is how a whole run gets thrown away. A stage that reports no roles
114
+ warns and runs unverified. `--skip-role-check` is for a target whose auth
115
+ reports roles somewhere pikku cannot read; findings from such a run may be seed
116
+ drift.
117
+
118
+ ## The other subcommands
119
+
120
+ | Command | What it answers |
121
+ | ------------------------------------ | ------------------------------------------------------------------------------------------ |
122
+ | `pikku persona list` | Who is declared — who each one is, what they may do, what they want |
123
+ | `pikku persona sync <environment>` | Which personas that environment will provision, with which roles, and why any were skipped |
124
+ | `pikku persona secret [personas...]` | Mint per-persona credentials from the root secret |
125
+
126
+ `sync` **reports**; it does not provision. The CLI has no connection to a
127
+ deployed environment's database, so the provisioning happens in the deployment —
128
+ pass the generated personas to `pikkuFabric` from `@pikku/better-auth`.
129
+
130
+ ## What NOT to do
131
+
132
+ - **Do not treat a clean run as a pass.** Nothing was asserted. Use scenarios
133
+ for the things that must hold.
134
+ - **Do not run a persona declared `runnable: false`**, or one whose `account`
135
+ names a provider. The first is someone who exists to be acted upon — running
136
+ her races the scenario that bans her — and the second needs a human at a
137
+ consent screen. Both are refused before sign-in rather than partway through.
138
+ - **Do not use `--api-url` to reach a production host from a non-production
139
+ environment.** The disposition guard reads the named environment's
140
+ `production` flag, not the url you pointed it at, so nothing will stop you.
141
+ - **Do not put any of the three credentials in `pikku.config.json`.** They are
142
+ environment variables.
143
+ - **Do not pass `--allow-approval` casually.** The endpoints behind it are the
144
+ ones the app marked as needing a human because they spend money.
145
+ - **Do not read a finding from a run started with `--skip-role-check` as a
146
+ product bug** until the roles are confirmed some other way.
147
+ - **Do not expect `--goals` to replace the persona's goals.** They are appended;
148
+ a run that replaces Susan's goals is not Susan.
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: pikku-seo
3
+ description: >-
4
+ On-page SEO rules for the app's PUBLIC pages: per-route head() titles and meta descriptions, Open Graph tags, one-h1 heading hierarchy, semantic/crawlable markup, JSON-LD on the landing page, and noindex for the logged-in area.
5
+ TRIGGER when: building or reworking any public page (landing, pricing, about, blog/content pages), writing page titles or meta tags, or the user asks about SEO / Google / discoverability / social sharing previews.
6
+ DO NOT TRIGGER when: working on logged-in /app screens (they are noindexed — only the one robots rule below applies), backend functions, database, or deployment.
7
+ installGroups: [client]
8
+ ---
9
+
10
+ # SEO Rules
11
+
12
+ Apps render SSR from the edge, so crawlers see full HTML — the ranking work is
13
+ getting the on-page signals right while you build. These rules apply to PUBLIC
14
+ routes only (the landing page and any marketing/content pages). The logged-in
15
+ `/app` area is private: it gets `noindex` and nothing else from this skill.
16
+
17
+ ## Per-route head() — every public route, no exceptions
18
+
19
+ Titles and descriptions live in TanStack Start's `head()` on the route, merged
20
+ root → leaf (the leaf's title/meta win). The root route already carries the
21
+ site-wide defaults and OG tags; every public page you add MUST override both:
22
+
23
+ ```tsx
24
+ export const Route = createFileRoute('/pricing')({
25
+ head: () => ({
26
+ meta: [
27
+ { title: 'Pricing — Acme Scheduling' },
28
+ {
29
+ name: 'description',
30
+ content:
31
+ 'Simple per-seat pricing for Acme Scheduling. Start free, upgrade when your team grows — no setup fees, cancel anytime.',
32
+ },
33
+ { property: 'og:title', content: 'Pricing — Acme Scheduling' },
34
+ { property: 'og:description', content: 'Simple per-seat pricing. Start free.' },
35
+ ],
36
+ }),
37
+ component: PricingPage,
38
+ })
39
+ ```
40
+
41
+ `head()` strings are plain strings (they do not go through the Mantine i18n
42
+ gate) — write real copy for THIS app, in the app's voice.
43
+
44
+ - **Title**: unique per page, 50–60 characters, the page's primary topic first,
45
+ brand at the end (`Topic — AppName`). The template's `__APP_TITLE__` default
46
+ must never survive the rebrand, on any page.
47
+ - **Description**: unique per page, 150–160 characters, states the concrete
48
+ value of the page in plain language — a reason to click, not a keyword list.
49
+ - **Dynamic public pages** (e.g. a public detail page) build both from loader
50
+ data: `head: ({ loaderData }) => ({ meta: [{ title: `${loaderData.name} — AppName` }, ...] })`.
51
+ - **Never invent URLs**: the deployed domain is unknown at build time, so do
52
+ NOT emit `canonical`, `og:url`, or `og:image` pointing at a made-up domain —
53
+ omit them (same principle as the `/api` serverUrl rule). `og:image` only if a
54
+ real asset exists in the app.
55
+
56
+ ## Logged-in area = noindex
57
+
58
+ The `/app` route (the authenticated layout route) gets exactly one meta entry:
59
+
60
+ ```tsx
61
+ head: () => ({ meta: [{ name: 'robots', content: 'noindex' }] })
62
+ ```
63
+
64
+ Never noindex a public page, and never put per-page SEO effort into `/app`
65
+ screens — they are invisible to crawlers by design.
66
+
67
+ ## Headings — exactly one h1 per page
68
+
69
+ - Every page has EXACTLY ONE h1 (`<Title order={1}>` in Mantine, `<h1>` in
70
+ Tailwind) and it names the page's primary topic — aligned with the title tag,
71
+ not identical boilerplate.
72
+ - Logical hierarchy below it: h1 → h2 → h3, no skipped levels, headings
73
+ describe the content under them. Never pick a heading level for its font
74
+ size — set the size on the correct level (`<Title order={2} fz="xs">`).
75
+
76
+ ## Crawlable, semantic markup
77
+
78
+ - Landmarks on public pages: `<nav>`, `<main>`, `<footer>` (Mantine: `component="nav"` etc.).
79
+ - Navigation between public pages uses real links (`<Link>`/`<a href>`) with
80
+ descriptive anchor text — crawlers follow hrefs; a `div onClick` navigation
81
+ is invisible to them. No public page may be orphaned: every public page is
82
+ reachable by link from the landing page (directly or via nav/footer).
83
+ - Every meaningful `<img>` has alt text describing the image; decorative images
84
+ get `alt=""`. Prefer descriptive file names for real assets.
85
+ - Readable URLs: public routes are lowercase, hyphen-separated, and named for
86
+ their content (`/pricing`, `/how-it-works`) — never `/page2` or query-param
87
+ navigation.
88
+
89
+ ## JSON-LD on the landing page
90
+
91
+ The landing page carries one structured-data script describing the product.
92
+ Only mark up what is visibly true on the page — never invent ratings, reviews,
93
+ or offers (fake schema is a Google penalty, not a boost):
94
+
95
+ ```tsx
96
+ head: () => ({
97
+ meta: [
98
+ /* title + description as above */
99
+ ],
100
+ scripts: [
101
+ {
102
+ type: 'application/ld+json',
103
+ children: JSON.stringify({
104
+ '@context': 'https://schema.org',
105
+ '@graph': [
106
+ { '@type': 'Organization', name: 'Acme Scheduling', description: '…' },
107
+ { '@type': 'WebSite', name: 'Acme Scheduling' },
108
+ ],
109
+ }),
110
+ },
111
+ ],
112
+ })
113
+ ```
114
+
115
+ Add further types only when the page genuinely IS that thing and shows the
116
+ required fields: `FAQPage` for a real FAQ section, `Article` for a blog post
117
+ (headline, datePublished, author), `Product`/`Offer` for a real price list.
118
+ Omit `url`/`logo` fields — deployed domain unknown (see "never invent URLs").
119
+
120
+ ## Verify
121
+
122
+ Open each PUBLIC page and read its `<head>`: a title, a meta description, and
123
+ exactly one `h1`. A public page is not done while any of the three is missing.
124
+ Signed-in pages under `/app` are exempt once `noindex` is set — they are not
125
+ indexed, so their head tags do not matter.
126
+
127
+ ## Don'ts
128
+
129
+ - No keyword stuffing — write for the reader; one clear topic per page.
130
+ - Don't duplicate the same title/description across pages (worse than absent).
131
+ - Don't render SEO-critical copy only after client-side effects — it must be
132
+ in the SSR HTML (loader data is fine; `useEffect`-fetched content is not).
133
+ - Don't add robots.txt/sitemap plumbing — the platform owns that layer.
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: pikku-service-backends
3
+ description: >-
4
+ Use when picking or wiring a backend for one of Pikku's core service interfaces — ContentService
5
+ (S3, Backblaze B2), QueueService (SQS), SecretService (AWS Secrets Manager, Redis, MongoDB),
6
+ SchemaService (AJV, cfworker), ChannelStore, EventHubStore, WorkflowService, SessionStore or
7
+ AgentRunService (Redis, MongoDB). Covers which backend to choose, what each one silently does
8
+ differently, and the failures they swallow. TRIGGER when: code uses S3Content, B2Content,
9
+ SQSQueueService, AWSSecrets, RedisChannelStore, RedisSecretService, MongoDBChannelStore,
10
+ PikkuMongoDB, AjvSchemaService or CFWorkerSchemaService, or the user asks how to store files,
11
+ secrets, channel state or sessions. DO NOT TRIGGER when: defining service factories themselves
12
+ (use pikku-services), SQL via Kysely (use pikku-kysely), or the Lambda/Cloudflare runtimes
13
+ themselves (use pikku-deploy).
14
+ installGroups: [core]
15
+ ---
16
+
17
+ # Pikku Service Backends
18
+
19
+ ## Agent Operating Procedure
20
+
21
+ Use this skill as an execution checklist, not reference material.
22
+
23
+ 1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
24
+ 2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
25
+ 3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
26
+ 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.
27
+ 5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
28
+
29
+ Constructor shapes and method signatures come from `pikku doc` — run
30
+ `pikku doc --ai` for the installed surface. This skill is the part the compiler
31
+ cannot tell you: which backend implements which interface, and what changes when
32
+ you swap one for another.
33
+
34
+ `pikku-services` covers how to build and wire a service. This covers what to put
35
+ behind the interface.
36
+
37
+ ## Pick a backend
38
+
39
+ | Interface | Backends | Package |
40
+ | --- | --- | --- |
41
+ | `ContentService` | `S3Content`, `B2Content` | `@pikku/aws-services`, `@pikku/backblaze` |
42
+ | `QueueService` | `SQSQueueService` | `@pikku/aws-services` |
43
+ | `SecretService` | `AWSSecrets`, `RedisSecretService`, `MongoDBSecretService` | `@pikku/aws-services`, `@pikku/redis`, `@pikku/mongodb` |
44
+ | `SchemaService` | `AjvSchemaService`, `CFWorkerSchemaService` | `@pikku/schema-ajv`, `@pikku/schema-cfworker` |
45
+ | `ChannelStore`, `EventHubStore` | Redis, MongoDB | `@pikku/redis`, `@pikku/mongodb` |
46
+ | `PikkuWorkflowService`, `WorkflowRunService` | Redis, MongoDB | `@pikku/redis`, `@pikku/mongodb` |
47
+ | `SessionStore`, `AgentRunService`, `DeploymentService` | Redis, MongoDB | `@pikku/redis`, `@pikku/mongodb` |
48
+ | `AgentStorageService`, `AgentRunStateService` | MongoDB **only** | `@pikku/mongodb` |
49
+
50
+ SQL is the third option for every store interface in that table —
51
+ `KyselyChannelStore`, `KyselyWorkflowService`, `KyselySecretService` and friends
52
+ live in `@pikku/kysely` and are covered by `pikku-kysely`, because using them
53
+ means writing queries.
54
+
55
+ Per-package detail: `references/aws.md`, `references/backblaze.md`,
56
+ `references/redis.md`, `references/mongodb.md`, `references/schema.md`.
57
+
58
+ ## What changes when you swap a backend
59
+
60
+ ### Redis and MongoDB are not interchangeable, in two ways
61
+
62
+ They cover almost the same interface list, but:
63
+
64
+ - **MongoDB has AI conversation storage and Redis does not.**
65
+ `MongoDBAgentStorageService` is the only implementation of
66
+ `AgentStorageService`/`AgentRunStateService`. A Redis-only deployment cannot
67
+ persist agent conversations.
68
+ - **Every MongoDB service needs `await init()`; no Redis service does.** `init()`
69
+ is what creates the collections and indexes. Constructing a
70
+ `MongoDBChannelStore` and using it without awaiting `init()` compiles and then
71
+ behaves like an unindexed collection — slow first, wrong later.
72
+
73
+ Redis services take the connection directly (an ioredis `Redis`, `RedisOptions`,
74
+ or a URL string). MongoDB services take a `Db`, which means a `PikkuMongoDB`
75
+ wrapper has to be constructed and initialised before any of them.
76
+
77
+ ### The two content backends share a design and a trap
78
+
79
+ `S3Content` and `B2Content` are close enough to swap, and both:
80
+
81
+ - treat `bucket` on every call as a **logical** bucket stored as a path prefix
82
+ (`${bucket}/${key}`) inside the one real bucket the config names. Do not
83
+ provision a bucket per logical bucket — the config takes exactly one.
84
+ - **ignore `visibility` on `getUploadURL`**.
85
+ - **swallow write failures**: `writeFile`, `copyFile` and `deleteFile` log and
86
+ return `false` rather than throwing, while the read paths throw. An ignored
87
+ return value is a silently lost file.
88
+
89
+ Where they diverge:
90
+
91
+ | | `S3Content` | `B2Content` |
92
+ | --- | --- | --- |
93
+ | Signing failure | **Fails open** — logs and returns the *unsigned* URL | Throws |
94
+ | `writeFile` memory | Streams | **Buffers the whole stream** to compute a SHA-1 |
95
+ | Client-side upload integrity | Presigned, expires at a fixed 3600s | `X-Bz-Content-Sha1: do_not_verify` — unverified |
96
+ | Credential rotation | Picked up by the SDK provider chain | Auth is cached for the instance's lifetime — construct a new `B2Content` |
97
+
98
+ The S3 fail-open is the one to design around: on a private CloudFront
99
+ distribution the client gets a 403, and on a public one you have just handed out
100
+ an unrestricted link. Validate `signConfig` at boot rather than trusting a throw.
101
+
102
+ ### Secret backends differ on whether the app can write
103
+
104
+ - **`AWSSecrets` is read-only.** `setSecret` and `deleteSecret` throw. Secrets
105
+ are managed out of band; the app only reads them.
106
+ - **Redis and MongoDB do envelope encryption** and can write, delete, and
107
+ `rotateKEK()`. Rotation requires `previousKey` to have been set — a service
108
+ constructed without it cannot rotate later without a redeploy.
109
+ - **Only MongoDB has audit hooks** (`audit`, `auditReads`).
110
+
111
+ `AWSSecrets` also collapses every failure — missing, denied, binary-only — into
112
+ the same `FATAL: Error finding secret: <id>`, with the real reason on the error's
113
+ `cause`. Read `cause` before concluding a secret is absent; `hasSecret` returns
114
+ `false` for any error and cannot distinguish the two either.
115
+
116
+ ### AJV and cfworker are not drop-in equivalents
117
+
118
+ Swapping them changes behaviour without changing types:
119
+
120
+ - **`useDefaults`**: AJV fills schema defaults into the validated object in
121
+ place. cfworker does not, so a field you relied on being defaulted arrives
122
+ `undefined` on Workers.
123
+ - **Recompilation**: AJV caches by name for the process lifetime — a second
124
+ `compileSchema` with the same name is a no-op. cfworker replaces the validator
125
+ when the schema value changes, which is what lets a dev hot-reload pick up
126
+ regenerated schemas. On AJV, restart the process instead.
127
+ - **Coercion is neither one's job.** `coerceTypes` is off; a query-string `"1"`
128
+ becomes `1` in the wiring layer, not here.
129
+
130
+ Both throw `UnprocessableContentError` (422) on a failed validation, and both
131
+ throw a **bare string** — `Missing validator for <name>` — for a *missing*
132
+ schema. It is not an `Error`, so `catch (e) { e.message }` reads `undefined`.
133
+ That almost always means codegen did not run.
134
+
135
+ Use cfworker on Cloudflare Workers: AJV compiles with `new Function`, which the
136
+ Workers runtime forbids.
137
+
138
+ ### SQS gives you no result back
139
+
140
+ `SQSQueueService` sets `supportsResults = false` and `getJob()` always throws —
141
+ the transport is fire-and-forget. So is the Azure Storage Queue backend. Reach
142
+ for BullMQ or PgBoss (see `pikku-wiring`) when a caller needs the job's result.
143
+
144
+ ## What NOT to do
145
+
146
+ - Do not ignore the boolean from `writeFile`, `copyFile` or `deleteFile`. Both
147
+ content backends report failure that way and neither throws.
148
+ - Do not rely on `S3Content.signURL` throwing. It fails open and hands back an
149
+ unsigned URL.
150
+ - Do not construct a MongoDB-backed service without awaiting `init()`.
151
+ - Do not assume AJV and cfworker validate identically — `useDefaults` alone
152
+ changes what your function receives.
153
+ - Do not provision one real bucket per logical bucket; the prefix is the bucket.
154
+ - Do not reach for SQS when a caller needs the result of the job.