@pikku/skills 0.12.42 → 0.12.44

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.42",
3
+ "version": "0.12.44",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -500,10 +500,9 @@ value for the address being signed in as and compares, so a credential minted
500
500
  for one persona is refused for every other, and the root itself is never a valid
501
501
  credential. A root under 32 characters refuses the endpoint outright rather than
502
502
  deriving weak credentials from it (the server log names the problem; the client
503
- is not told which). Callers rarely derive by hand — `pikku dev` mints one per
504
- persona into `VITE_DEV_ACTOR_SECRETS` for the browser switcher, `pikku persona
505
- secret <id>` mints them for a run, and the two `PersonaSignIn` implementations
506
- derive on the fly.
503
+ is not told which). Callers rarely derive by hand — `pikku persona secret <id>`
504
+ mints them for a run, and the two `PersonaSignIn` implementations derive on the
505
+ fly. The browser switcher holds none: it signs in through `/sign-in/persona`.
507
506
 
508
507
  **Which command is running decides whether it works, not whether a secret is
509
508
  set.** `pikku dev` sets `PIKKU_DEV_ACTOR_SIGN_IN` and mints an ephemeral
@@ -559,9 +558,11 @@ actor`. So the secret cannot take over a **real user's** account — the blast
559
558
  paragraph above. The comparison is constant-time and length-hiding, so a wrong
560
559
  credential leaks neither the length nor a prefix of the right one.
561
560
 
562
- This is the endpoint `pikku scenario` signs its actors in through, and the one
563
- the frontend dev switcher posts to — see `pikku-scenario` for declaring the
564
- actors and `pikku-react` for `useDevActors()`.
561
+ This is the endpoint `pikku scenario` signs its actors in through. The frontend
562
+ switcher does not use it: it lists from `/sign-in/personas` and posts a persona
563
+ id to `/sign-in/persona`, both served by
564
+ `pikkuActor({ personaSignIn: { personas, featureFlags } })` with no credential — see
565
+ `pikku-scenario` for the setup and `pikku-react` for `useDevActors()`.
565
566
 
566
567
  ### Provisioning personas
567
568
 
@@ -573,17 +573,13 @@ frontend running against a dead API looks exactly like an app bug, so if every
573
573
  request fails, check that both halves came up.
574
574
 
575
575
  **Start the stack through `bun run dev`, not by launching `vite` or `pikku dev`
576
- yourself.** The dev script reads the personas, derives one credential per
577
- persona from `SCENARIO_ACTOR_SECRET`, and hands both to the frontend as
578
- `VITE_DEV_ACTORS` / `VITE_DEV_ACTOR_SECRETS`. Vite reads those once, at boot. A
579
- frontend started any other way — or restarted by hand later — has an empty
580
- actor list, and the switcher silently disappears from every page. If you do
581
- start the frontend on its own (say :3000 is taken by another project), you owe
582
- it three things: the two `VITE_DEV_*` values the dev script would have computed,
583
- and `VITE_API_PROXY` pointing at your API — the dev proxy defaults to
584
- `http://localhost:3000`, so beside another project's server your sign-ins go to
585
- _its_ API and come back `401 Invalid actor secret`, which reads like a bad
586
- credential rather than the wrong server.
576
+ yourself.** The "Sign in as …" switcher asks the API for its personas at
577
+ runtime, so the frontend needs nothing baked in — but it does need to reach
578
+ _your_ API. If you start the frontend on its own (say :3000 is taken by another
579
+ project), point `VITE_API_PROXY` at your API: the dev proxy defaults to
580
+ `http://localhost:3000`, so beside another project's server the switcher lists
581
+ _its_ personas, or none, which reads like a missing switcher rather than the
582
+ wrong server.
587
583
 
588
584
  The `--bun` in `bunx --bun pikku …` is load-bearing — keep it. Without it the
589
585
  CLI's `#!/usr/bin/env node` shebang hands the process to whatever Node is on
@@ -709,6 +705,21 @@ new RPC and does not re-run `afterStart`, so a fresh function answers 404 and
709
705
  anything provisioned at boot is missing — failures that read like a wiring bug
710
706
  and are nothing but a stale process.
711
707
 
708
+ **Run the whole suite, not the milestone's own scenarios.** The milestone's
709
+ scenarios are the ones you wrote to pass; the regression lives in someone
710
+ else's. Tightening what "archived" means is a one-function change that reads as
711
+ local and quietly breaks the milestone-01 scenario nobody re-ran.
712
+
713
+ **Restart the server after adding a function, and never edit one while a run is
714
+ in flight.** Hot reload does not register a new RPC and does not re-run
715
+ `afterStart`, so a fresh function answers 404 and anything provisioned at boot
716
+ is missing — failures that read like a wiring bug and are nothing but a stale
717
+ process. The same reload is what makes a run unrepeatable if you edit during
718
+ it: a browser pass is long enough to feel like free time, and a schema touched
719
+ at minute four hot-reloads into a half-generated contract, so every scenario
720
+ after that point fails on something you have already fixed. Wait for the run or
721
+ kill it — a run you edited under is not a result.
722
+
712
723
  ### 7a. Coverage — which functions have actually been run
713
724
 
714
725
  Green scenarios tell you the journeys you wrote still work. They say nothing
@@ -1,172 +1,124 @@
1
1
  ---
2
2
  name: pikku-changes
3
- description: 'Work a project''s changes queue — the todo list someone filed by walking a deployed stage. Covers `pikku fabric changes list|claim|show|ask|shot|done`, when to ask a question instead of guessing, and how to offer options as images. TRIGGER when: the user says "work the changes", "pick up the changes queue", names a change by its #number, or you are otherwise idle in a repo that has a pikkufabric.config.json. DO NOT TRIGGER for git changes, diffs or changelogs, and not for deploying or debugging a stage — use pikku-fabric for those.'
3
+ description: 'Work a Fabric project''s changes queue — the todo list someone filed by circling things on a deployed stage. Covers `pikku fabric changes next|claim|show|ask|shot|done`: waiting for work without polling, asking instead of guessing, offering options as images, one commit per item. TRIGGER when: the user says "run the pikkufabric changes", "run the changes against <stage>", "work the changes (queue)", "watch the changes", "pick up the changes", names a change by its #number, or you are otherwise idle in a repo that has a pikkufabric.config.json. DO NOT TRIGGER for git changes, diffs or changelogs, and not for deploying or debugging a stage — use pikku-fabric for those.'
4
4
  installGroups: [fabric]
5
5
  ---
6
6
 
7
7
  # Working a changes queue
8
8
 
9
- Someone walked the deployed app and circled twenty things. Each one is a row with their
10
- words, a picture of what they were looking at, and the elements the circle enclosed. You
11
- have the repo and the app running locally. Your job is to empty the queue without making
12
- them regret filing.
9
+ Someone walked the deployed app and circled things. Each item is their words, a
10
+ screenshot of what they saw, and the elements the circle enclosed. You have the repo.
11
+ Empty the queue without making them regret filing.
13
12
 
14
- ## The loop
15
-
16
- Every argument is a flag; nothing is positional. `--json` works on any of them. The
17
- project comes from the local `pikkufabric.config.json`, so `--project-id` is only needed
18
- when you are not in the checkout.
13
+ Run every command from the checkout: the project comes from `pikkufabric.config.json`.
14
+ `--json` works on all of them. Items are addressed as `2`, `#2` or their uuid.
19
15
 
20
- ```bash
21
- pikku fabric changes list --pickup-only --json
22
- pikku fabric changes claim --change-ids <id>,<id> --title "Checkout pass" --claimed-by claude-code
23
- pikku fabric changes show --change-id <id>
24
- pikku fabric changes ask --change-id <id> --question "…" --option "…" --option "…" --author-name claude-code
25
- pikku fabric changes shot --change-id <id> --label "Bigger" --kind option --image a.png
26
- pikku fabric changes done --change-id <id> --note "What you did"
27
- ```
16
+ ## Which stage
28
17
 
29
- `list --pickup-only` is the one a harness wants: it skips items still inside the grace
30
- window, so a batch someone is mid-way through typing is picked up together rather than item
31
- by item as it lands.
18
+ The queue is per project. "Against develop" or a pasted stage URL narrows it:
19
+ `--stage` takes a branch, the stage URL (as filed, path optional) or a stage id. With
20
+ no stage named, work the whole project. An unknown name prints the stages there are.
32
21
 
33
- Deciding what belongs together is yours: `claim` with `--change-ids` and no `--group-id`
34
- forms the group. Claim an existing one with `--group-id`.
22
+ ## The loop
35
23
 
36
- Claim before working. The lease expires (30 minutes by default, `--lease-minutes` to
37
- change it), so an abandoned claim returns to the queue rather than parking the work
38
- forever — but a second harness picking up something you are halfway through is the failure
39
- this prevents.
24
+ **Never poll.** No `sleep` loops, no repeated `list`, no re-running `show` to see if
25
+ something changed. `next` does the waiting and exits only when there is work.
40
26
 
41
- ## Reading an item
27
+ 1. Start `next` as a **background** command, and stop there until it exits:
42
28
 
43
- `show` gives you four things, in descending order of trustworthiness:
29
+ ```bash
30
+ pikku fabric changes next --stage develop --claim --claimed-by claude-code
31
+ ```
44
32
 
45
- 1. **Their words.** The title and body are the requirement. Everything else is evidence.
46
- 2. **The screenshot.** What they actually saw, at their width, with their data. When the
47
- other addresses disagree with the picture, the picture is right.
48
- 3. **The circled elements** — a testid, a source anchor, a CSS path. The testid greps
49
- straight to a component because it is the i18n message key.
50
- 4. **The source anchor**, printed as `src/routes/app.orders.tsx:42 as of a91c4e2`. That line
51
- number is where the JSX was **at that commit**. Read it as a starting point and find
52
- today's equivalent; never edit line 42 of today's file because the anchor said 42.
33
+ It waits out the grace window (a just-filed item is held about a minute so a batch
34
+ being typed arrives together), claims what is ready as one group, prints it, and
35
+ exits. It also wakes when someone answers a question you asked under that
36
+ `--claimed-by`.
53
37
 
54
- Resolution is a guess and the panel says so. If the circle and the anchor point at
55
- different things, believe the circle.
38
+ 2. When it exits, read the exit code:
56
39
 
57
- ## When to ask
40
+ | code | meaning | do |
41
+ | --- | --- | --- |
42
+ | 0 | work printed (claimed, and/or `Answered`) | work it, then step 3 |
43
+ | 2 | `--timeout`/`--once` found nothing | stop, or restart `next` |
44
+ | 3 | session refused | tell the user to run `pikku fabric login`; stop |
45
+ | 1 | anything else (bad `--stage`, fabric down for minutes) | report the message; stop |
58
46
 
59
- Ask when the item admits more than one reasonable implementation and you would be **picking
60
- for them**. Do not ask to confirm something the item already says.
47
+ 3. For each item: `show` → fix → commit → `done`, or `ask` and move on. Then start
48
+ `next` again, in the background.
61
49
 
62
- Ask:
63
- - "Make the total stand out" — bigger, bolder, coloured, or moved above the fold?
64
- - "This should be faster" — is it the spinner, the request, or the number of steps?
65
- - Anything that changes what data is stored, what an existing user sees, or what something costs.
50
+ Without `--claim` it only reports what is claimable; claim it yourself:
66
51
 
67
- Do not ask:
68
- - "Should I use flexbox or grid?" — that is yours.
69
- - "Do you want me to fix the typo?" — they filed it; fix it.
70
- - "Can you confirm you want the button blue?" — they said blue.
52
+ ```bash
53
+ pikku fabric changes claim --change-ids 3,4 --title "Checkout pass" --claimed-by claude-code
54
+ ```
71
55
 
72
- A question costs them a context switch, not typing. That is the budget you are spending.
56
+ A `claim` refused with a 409 says per item why (held, claimed by someone else, done).
57
+ For held items, run `next --claim` rather than retrying. The lease is 30 minutes
58
+ (`--lease-minutes`); an abandoned claim returns to the queue by itself.
73
59
 
74
- ## What a good question looks like
60
+ ## Reading an item
75
61
 
76
- One decision. Their vocabulary, not the codebase's. And the choices in `--option`, not in
77
- the sentence.
62
+ `pikku fabric changes show 3` gives, most trustworthy first:
78
63
 
79
- > **Bad:** "How would you like me to handle the ambiguity in the checkout total component's
80
- > emphasis requirement?"
81
- >
82
- > **Good:** `--question "Make the total stand out — which way?"`
83
- > `--option "Bigger" --option "Move it above the delivery line"`
64
+ 1. **Their words.** The title and body are the requirement. Everything else is evidence.
65
+ 2. **The screenshot.** What they saw, at their width, with their data. When the other
66
+ addresses disagree with the picture, the picture is right.
67
+ 3. **The circled elements**: a testid (greps straight to a component, it is the i18n
68
+ key), a source anchor, a CSS path.
69
+ 4. **The source anchor**, `src/routes/app.orders.tsx:42 as of a91c4e2`, is where the JSX
70
+ was at *that* commit. Find today's equivalent; never edit line 42 because it said 42.
84
71
 
85
- **A choice written into the prose is not a choice.** Every `--option` becomes a button in
86
- the panel and the console, and clicking one records the answer; a question that says
87
- "(a) build it, (b) leave existing bookings, (c) hold" makes them re-type in free text what
88
- they should have been able to click, and leaves you parsing prose to find out which one
89
- they meant. If you can enumerate them in the sentence, you can pass them as flags.
72
+ ## When to ask
90
73
 
91
- Pass them even when there are only two, and even when one is "hold until I check" — that
92
- last one is a real option and it is the one most often left off. The filer can always
93
- choose "say something else", so the list constrains nothing.
74
+ Ask when the item admits more than one reasonable implementation and you would be
75
+ picking for them — "make the total stand out" (bigger? bolder? moved?), anything that
76
+ changes stored data, what an existing user sees, or what something costs. Do not ask
77
+ what the item already says, or implementation choices that are yours.
94
78
 
95
- Batch per group. Three questions about one checkout flow go out together; three separate
96
- asks about the same screen is three interruptions for one context switch.
79
+ One decision, in their vocabulary, with the choices as `--option` flags — each becomes
80
+ a button. Include "hold until I check" when it is real. Batch questions per group.
97
81
 
98
- Then **park it**. `ask` flips the item to `needs_answer` and you move to the next item. Do
99
- not sit waiting — pick answers up on your next `show`, and bound your polling so an
100
- unanswered item does not spin forever.
82
+ ```bash
83
+ pikku fabric changes ask --change-id 3 --question "Make the total stand out — which way?" \
84
+ --option "Bigger" --option "Move it above the delivery line" --author-name claude-code
85
+ ```
101
86
 
102
- ## When to show instead of ask
87
+ Then **park it** and move on. The answer wakes `next` (same `--claimed-by`); it prints
88
+ under `Answered`, and `show` has the reply.
103
89
 
104
- If the answer is visual and you can build it, build all of them and attach images:
90
+ If the answer is visual and you can build it, build each variant, screenshot all of
91
+ them in one pass at one width (baseline included), and attach them — the panel turns
92
+ `--kind option` shots into a pick-one:
105
93
 
106
94
  ```bash
107
- pikku fabric changes shot --change-id <id> --label "Bigger" --kind option --image a.png
108
- pikku fabric changes shot --change-id <id> --label "Above the line" --kind option --image b.png
95
+ pikku fabric changes shot --change-id 3 --label "Bigger" --kind option --image a.png
109
96
  ```
110
97
 
111
- The panel turns a set of `option` attachments into a pick-one they open full-screen, and
112
- picking one writes the choice into the thread. Capture every variant in **one pass at one
113
- width**, including the baseline — variants shot at different sizes are not comparable, and
114
- comparing is the whole point.
115
-
116
- `--kind evidence` is the other use: a picture that proves something, rendered inline rather
117
- than as a choice.
98
+ `--kind evidence` is a picture that proves something, shown inline.
118
99
 
119
100
  ## Committing
120
101
 
121
- One item, one commit. `done` records a single `head_commit`, and that sha is what a human
122
- reverts when they change their mind — so an item folded in with three others cannot be
123
- undone without taking the other three with it. Land unrelated work separately.
124
-
125
- The subject carries the short id the way a GitHub issue number does, and the uuid goes in a
126
- trailer so `git log --grep` has an exact handle:
102
+ One item, one commit — `done` records one sha, and that is what a human reverts. The
103
+ subject carries the short id; the uuid goes in a trailer:
127
104
 
128
105
  ```
129
- feat(login): #4 make the sign-in heading brown
106
+ fix(booking): #7 stop the date picker closing on the first click
130
107
 
131
108
  Change-Id: 0f3c8a12-9b44-4d2e-8f01-27c6a1d9e5b3
132
109
  ```
133
110
 
134
- Both ids come from `show`. The type and scope are the usual conventional-commit ones —
135
- `feat`, `fix`, `style`, `refactor` — with the scope naming the screen or area they were
136
- looking at, not the file you edited.
137
-
138
- More:
139
-
140
- ```
141
- fix(booking): #7 stop the date picker closing on the first click
142
- style(nav): #12 tighten the spacing around the logo
143
- ```
144
-
145
- Reverting one later is then:
146
-
147
- ```bash
148
- git revert $(git log --grep="Change-Id: <uuid>" --format=%H -1)
149
- ```
111
+ Scope names the screen they were looking at, not the file you edited.
150
112
 
151
113
  ## Finishing
152
114
 
153
- `done` records the branch and commit that closed it, which is what strikes the item through
154
- on the page it was filed on and tells them where the fix landed. Both default to the
155
- checkout you are standing in, so run it from there and let it read git:
156
-
157
115
  ```bash
158
- pikku fabric changes done --change-id <id> \
159
- --note "What you did, for whoever reads the thread later"
116
+ pikku fabric changes done --change-id 7 --note "What you did, for whoever reads the thread"
160
117
  ```
161
118
 
162
- `--branch` and `--head-commit` override them, for the case where the fix landed somewhere
163
- other than where you are. Never type a sha by hand — one that does not exist points the
164
- filer at nothing.
165
-
166
- An item you decided not to do is not `done`. Say why in the thread and leave it for a human
167
- to dismiss.
168
-
169
- ## Scope
119
+ Branch and commit default to the checkout you are in — run it there, never type a sha.
120
+ An item you decided not to do is not `done`: say why in the thread and leave it for a
121
+ human to dismiss.
170
122
 
171
- Writes need the `changes:project:write` scope on your bearer. `list` and `show` are reads.
172
- The project comes from the local `pikkufabric.config.json`, so run these from the checkout.
123
+ Writes need the `changes:project:write` scope; `list`, `show` and `next` without
124
+ `--claim` are reads.
@@ -477,17 +477,16 @@ reviewer has no seed password, so without the control they are locked out of the
477
477
  app they were asked to look at.
478
478
 
479
479
  Satisfy it with `<DevActorSwitcher />` from `@pikku/mantine/dev`, or with your
480
- own UI built on `useDevActors()` from `@pikku/react` — validate accepts either
481
- call site as evidence, so custom rendering passes. See **pikku-react** for the
482
- props and **pikku-scenario** for where the actor list comes from.
480
+ own UI built on `useDevActors()` or `signInAsPersona()` from `@pikku/react` —
481
+ validate accepts any of those call sites as evidence, so custom rendering
482
+ passes. Either way the server needs `personaSignIn` on `pikkuActor`; see
483
+ **pikku-scenario** for it and **pikku-react** for the props.
483
484
 
484
485
  The validator also accepts the shapes that predate the package — a hand-rolled
485
486
  `signInAsActor()` or a literal `POST /auth/sign-in/actor` — so an older app does
486
487
  not fail the build. **Treat that as a grace period, not the target: migrate those
487
- to `<DevActorSwitcher />`.** The hand-copied version is exactly the duplication
488
- the package exists to remove, and the copies drift — the ones that prompted this
489
- had already diverged on the `import.meta.env.DEV` gate that keeps the shared
490
- secret out of production bundles.
488
+ to `<DevActorSwitcher />`.** They put a per-persona credential in the frontend
489
+ bundle, which the persona endpoint exists to avoid.
491
490
 
492
491
  Do **not** satisfy it with Better Auth's `/dev/quick-login`. That is a different
493
492
  endpoint with a different purpose — one fixed admin, not the declared personas —
@@ -6,7 +6,7 @@ description: >-
6
6
  direct usePikkuRPC / usePikkuFetch calls, realtime subscriptions, agent and workflow hooks, and
7
7
  the dev actor switcher. TRIGGER when: writing a React component that fetches or mutates backend
8
8
  data, wiring PikkuProvider, paginating, running or tracking a workflow from the client, or
9
- asking about useDevActors / VITE_DEV_ACTORS / quick login. DO NOT TRIGGER when: working on the
9
+ asking about useDevActors / DevActorSwitcher / quick login. DO NOT TRIGGER when: working on the
10
10
  backend (use pikku-wiring), defining the workflow itself (use pikku-workflow), or writing
11
11
  user-facing copy (use pikku-i18n).
12
12
  installGroups: [client]
@@ -251,14 +251,9 @@ their own sandbox.
251
251
  ```tsx
252
252
  import { useDevActors } from '@pikku/react'
253
253
 
254
- const { actors, signInAs, pendingEmail, isPending, error } = useDevActors({
255
- // Gate both reads on the bundler's dev flag so no credential can reach a
256
- // production bundle. The sandbox dev server bakes them from your personas.
257
- actors: import.meta.env.DEV ? import.meta.env.VITE_DEV_ACTORS : undefined,
258
- secrets: import.meta.env.DEV
259
- ? import.meta.env.VITE_DEV_ACTOR_SECRETS
260
- : undefined,
254
+ const { actors, signInAs, pendingId, isPending, error } = useDevActors({
261
255
  apiUrl: apiUrl(),
256
+ app: appSlug,
262
257
  onSignedIn: () => navigate({ to: '/' }),
263
258
  })
264
259
  ```
@@ -267,21 +262,19 @@ const { actors, signInAs, pendingEmail, isPending, error } = useDevActors({
267
262
  `<DevActorSwitcher />` from `@pikku/mantine/dev` — a separate entry point from
268
263
  `@pikku/mantine/core`, whose contract is "drop-in alias for `@mantine/core`"
269
264
  and so must not export components Mantine has no counterpart for.
270
- - **`secrets` is `{ address: credential }`, not one shared value** — a
271
- credential opens the one persona it was minted for (see
272
- **pikku-auth**). `actors` is empty unless the host supplied both a list
273
- and the credentials for it, and an actor with no credential is not offered, so
274
- a production build renders nothing without you testing for it.
275
- - **It takes `onSignedIn` rather than a router**, and takes the env values rather
276
- than reading them, because how env is spelled is a bundler fact
277
- (`import.meta.env.VITE_*` vs `process.env.NEXT_PUBLIC_*`).
278
- - The underlying `signInAsActor()` and `parseDevActors()` are exported too, for a
279
- non-React caller. The endpoint only accepts rows flagged `actor: true`, so it
280
- can never impersonate a real user — see **pikku-auth**.
281
-
282
- Do not hand-write the `devActors()` / `signInAsActor()` pair per app; that
283
- copy-paste, including the `import.meta.env.DEV` gate, is exactly what this
284
- replaced.
265
+ - **No credential reaches the bundle.** It lists from `/auth/sign-in/personas`
266
+ and `signInAs(id)` posts only the persona id to `/auth/sign-in/persona`. The
267
+ server decides who is offered and who may sign in, and offers nobody in
268
+ production — see **pikku-scenario** for `personaSignIn`.
269
+ - **It takes `onSignedIn` rather than a router**, since every app lands
270
+ somewhere different.
271
+ - `listDevActors()` and `signInAsPersona()` are exported too, for a non-React
272
+ caller. The endpoint only
273
+ signs in rows flagged `actor: true`, so it can never impersonate a real user —
274
+ see **pikku-auth**.
275
+
276
+ Do not hand-write the list-and-sign-in pair per app; that copy-paste is exactly
277
+ what this replaced.
285
278
 
286
279
  ### Linking from a Mantine element: `renderRoot`, not `component`
287
280
 
@@ -83,13 +83,17 @@ Declared actors are not only for automated runs. `signInPath` is Better Auth's
83
83
  frontend gets a one-click "Sign in as …" switcher over the **same** list, and an
84
84
  app can be reviewed as each kind of user without anyone knowing a seed password.
85
85
 
86
- The dev server bakes both halves into the frontend from the declared
87
- personas — the sandbox's, or the template's `bun run dev`, never `pikku dev`: `VITE_DEV_ACTORS` (the JSON actor list) and `VITE_DEV_ACTOR_SECRETS`
88
- (`{ email: credential }`, one per persona — `SCENARIO_ACTOR_SECRET` itself never
89
- goes in a bundle; see **pikku-auth**). Neither var is set in a production
90
- build, so the control renders nothing there — but gate the reads on your
91
- bundler's dev flag anyway (`import.meta.env.DEV ? … : undefined`) so no
92
- credential reaches a production bundle in the first place.
86
+ The switcher holds no credential. It lists personas from
87
+ `/auth/sign-in/personas` and signs in by posting only a persona id to
88
+ `/auth/sign-in/persona`; the server resolves the address. One server piece
89
+ serves both, in the auth config:
90
+
91
+ ```ts snippet:personaSignIn
92
+ ```
93
+
94
+ It is always open under `pikku dev`. A deployed stage needs actor
95
+ sign-in opted in **and** its `devSwitcher` feature flag on; production never
96
+ has the opt-in, so it lists nobody and refuses every persona sign-in.
93
97
 
94
98
  Do not hand-roll the switcher: `useDevActors()` (`pikku-react`, a separate install) is the logic and
95
99
  `<DevActorSwitcher />` from `@pikku/mantine/dev` is a ready rendering of it.
@@ -98,52 +102,16 @@ one — without it a reviewer is locked out of their own sandbox.
98
102
  When the switcher is missing, it is one of three things, and none of them
99
103
  errors:
100
104
 
101
- - **The frontend was not started by the dev script.** The two `VITE_DEV_*` vars
102
- are computed by `bun run dev` and read by vite once, at boot. A bare `vite dev`
103
- — including one restarted by hand — has an empty list and renders nothing.
104
- - **`SCENARIO_ACTOR_SECRET` is not in `.env`.** No root secret, no per-persona
105
- credentials, and the switcher filters out every actor it cannot sign in.
105
+ - **The list is empty.** On a deployed stage that is the gate
106
+ doing its job — check the opt-in and the `devSwitcher` flag. Locally, check
107
+ the personas declare an `email` (via `scenarios.emailDomain`) and are not
108
+ `runnable: false`.
109
+ - **`personaSignIn` is missing, or the frontend calls another API.** A dev
110
+ proxy (`VITE_API_PROXY`, default `http://localhost:3000`) that points at
111
+ another project's API lists that project's personas, or none.
106
112
  - **It is not mounted on the page you are looking at.** The template mounts it
107
113
  on the login screen. A public homepage that replaces the `/` → `/app`
108
114
  redirect needs its own `<DevActorSwitcher />` in the public layout.
109
115
 
110
- When the switcher is there but signing in fails with `401 Invalid actor
111
- secret`, check which server answered before checking the secret: a frontend
112
- whose dev proxy (`VITE_API_PROXY`, default `http://localhost:3000`) points at
113
- another project's API sends the sign-in there.
114
-
115
- **A runner of your own that starts vite has to bake them itself**, from the
116
- generated persona meta (`<outDir>/workflow/personas.gen.json`, which already
117
- carries the derived `email`):
118
-
119
- ```js
120
- const personas = Object.values(JSON.parse(readFileSync(personasPath, 'utf8')))
121
-
122
- env.VITE_DEV_ACTORS = JSON.stringify(
123
- personas.map(({ id, email, name, jobTitle }) => ({
124
- key: id,
125
- email,
126
- name,
127
- jobTitle: jobTitle ?? '',
128
- }))
129
- )
130
- env.VITE_DEV_ACTOR_SECRETS = JSON.stringify(
131
- Object.fromEntries(
132
- await Promise.all(
133
- personas.map(async ({ email }) => [
134
- email,
135
- await deriveActorSecret(env.SCENARIO_ACTOR_SECRET, email),
136
- ])
137
- )
138
- )
139
- )
140
- ```
141
-
142
- **Set `SCENARIO_ACTOR_SECRET` yourself**, at least 32 characters, in the
143
- environment both processes read. Left unset, `pikku dev` mints an ephemeral root
144
- for its own run that a separately spawned vite cannot see, so the two derive
145
- from different roots: the switcher renders every persona and each click is
146
- refused, which reads as a broken login rather than missing configuration. On a
147
- brand-new project the persona file does not exist until the first `pikku dev`
148
- codegen, after vite has baked an empty list — watch it and restart the frontend
149
- when it changes.
116
+ When the switcher lists personas but a click 404s, that persona has no `email`
117
+ or is `runnable: false`.
@@ -105,6 +105,9 @@ subscribe to its events as `<source>:<event>`:
105
105
  ```
106
106
 
107
107
  - The route is `POST /webhooks/<name>` unless `method`/`route` say otherwise.
108
+ `method` may be a list, e.g. `['get', 'post']` for a provider that verifies
109
+ the URL with a GET and delivers events with a POST, or `['head', 'post']`
110
+ for one that checks the URL with a HEAD.
108
111
  It needs no session.
109
112
  - `events` maps each event name to a schema. An event that fails its schema is
110
113
  logged and dropped; so is one no `wireTrigger` listens for. Both still get a
@@ -126,9 +129,12 @@ id?, data }] }`, or `{ respond: { status, body } }` for a handshake. Throwing
126
129
  even on queues that ignore job ids, and records each attempt and its last
127
130
  error. Its `webhookReceipt` table comes from `pikku db generate`; `pikku dev`
128
131
  and `pikku serve` use it when a Kysely database is configured.
129
- - `receive` sees singleton services without `secrets`: read the signing secret
130
- in a service method, and declare it with `defineSecret`. Name it in `secret`
131
- so deploy knows where `setup` should store it.
132
+ - `receive` sees singleton services without `secrets`. Declare the signing
133
+ secret with `defineCredential({ type: 'singleton', ... })` and hold it as
134
+ `WebhookSigningSecret.fromCredential(provider, credentialService, name)`
135
+ from `@pikku/core/hmac`; `receive` calls `await signingSecret.load()` and
136
+ checks against what it returns. A handshake that hands over the secret
137
+ (Asana) stores it with `credentialService.set`.
132
138
 
133
139
  `check`, `setup` and `teardown` register the route with the provider. Each gets
134
140
  `{ url, label, events, previous? }`, where `events` are only the ones some
@@ -142,10 +148,9 @@ pikku webhooks teardown --url https://api.example.com --labelPrefix shop:prod --
142
148
 
143
149
  Each prints one JSON line per source (`ok`, `missing`, `drifted`, `created`,
144
150
  `updated`, `unchanged`, `manual`, `deleted`, `absent`, `skipped`, `failed`). `setup` only
145
- runs where `check` does not report `ok`, and a `setup` that returns a new signing
146
- secret prints it with the `secret` name to store it under. In CI pass
147
- `--secretsOut <file>`: secrets are written there (mode 600) as
148
- `{ secretName: secret }` and left out of stdout. Any step can be
151
+ runs where `check` does not report `ok`. A `setup` that gets a signing secret
152
+ from the provider stores it with `credentialService.set`, and `teardown`
153
+ deletes it, so a new secret needs no deploy and never reaches stdout. Any step can be
149
154
  `ref('<addon>:<fn>')` to use an addon's implementation.
150
155
 
151
156
  ## Usage Patterns