@pikku/skills 0.12.37 → 0.12.39

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.
@@ -0,0 +1,119 @@
1
+ # An app on top of an OpenAPI spec
2
+
3
+ The spec becomes an addon first — the `pikku-addon` skill's
4
+ `references/openapi.md` covers generating, verifying and checking it against
5
+ the real API. This reference is the decision that shapes the app around it:
6
+ **whose credentials reach the upstream**, and what the person signs in with.
7
+
8
+ ## Pick the auth mode
9
+
10
+ | The upstream… | Mode | Command | Who signs in, and how |
11
+ | ------------------------------------------------------------------------ | ----------------- | ----------------------------- | ------------------------------------------------------- |
12
+ | Is where the users already have accounts, and has a login endpoint | Delegated | `--auth-config <file>` | Users sign in with their upstream login; no new account |
13
+ | Takes a per-user API key, bearer token or basic auth | Per-user key | (the default) | App account, then a Connect screen for the key |
14
+ | Uses OAuth2 | Per-user OAuth | (the default) | App account, then Connect via the OAuth consent |
15
+ | Is used on everyone's behalf with one key the business owns | Shared secret | `--auth shared` | App account only; the key is a secret |
16
+ | Takes no auth | None | `--auth none` | App account only |
17
+
18
+ Per-user is the default because the upstream then enforces each person's own
19
+ permissions. A shared secret acts with one identity for everyone, so the
20
+ install exposes only its reads; widen that deliberately, never by default.
21
+
22
+ The mode comes from the spec's `securitySchemes` unless a flag overrides it.
23
+ Many real specs describe auth only in prose. The generator then refuses rather
24
+ than guessing. Read the API's docs and pass the flag that is true.
25
+
26
+ **Delegated is the right call whenever the upstream is the system of record for
27
+ who the users are** — an ERP, a CRM, a helpdesk the whole team already logs in
28
+ to. The app then has no separate sign-up, the upstream token is stored per user
29
+ at sign-in, and every call acts as that user.
30
+
31
+ ## The auth-config file
32
+
33
+ JSON, passed with `--auth-config`. Every field is optional except where noted.
34
+
35
+ ```json
36
+ {
37
+ "headerName": "DOLAPIKEY",
38
+ "headerFormat": "raw",
39
+ "extraHeaders": { "Origin": "https://tenant.example.com" },
40
+ "delegated": {
41
+ "loginPath": "/login",
42
+ "loginMethod": "post",
43
+ "credentials": ["login", "password"],
44
+ "fields": { "login": "login", "password": "password" },
45
+ "encoding": "json",
46
+ "tokenPath": "success.token",
47
+ "expiresAtPath": "success.expires",
48
+ "identity": { "path": "/users/info", "method": "get" },
49
+ "claims": {
50
+ "source": "identity",
51
+ "externalId": "id",
52
+ "email": "email",
53
+ "name": ["firstname", "lastname"],
54
+ "role": "admin",
55
+ "tenantId": "entity"
56
+ },
57
+ "emailTemplate": "{login}@{host}",
58
+ "roles": { "1": "dolibarr-admin" }
59
+ }
60
+ }
61
+ ```
62
+
63
+ Top level — how every call authenticates:
64
+
65
+ | Field | Meaning |
66
+ | -------------- | ---------------------------------------------------------------------------------------------------- |
67
+ | `headerName` | The header the API reads the token or key from. Setting it alone means a per-user API key |
68
+ | `headerFormat` | `raw` sends the bare value, `bearer` prefixes `Bearer `. Default: bearer for `Authorization`, else raw |
69
+ | `extraHeaders` | Static headers on every request, login included — for upstreams that route on a header |
70
+ | `delegated` | Present when users sign in with their upstream login |
71
+
72
+ `delegated`:
73
+
74
+ | Field | Meaning |
75
+ | ---------------- | --------------------------------------------------------------------------------------------------------- |
76
+ | `loginPath` | Required. The spec-relative login operation |
77
+ | `loginMethod` | Default `post` |
78
+ | `credentials` | What the sign-in form collects: `login` (a username or email), `email`, `password`, `apiKey`. Default `["email","password"]` |
79
+ | `fields` | The upstream's name for each credential, e.g. `{ "login": "username" }` |
80
+ | `encoding` | `json`, `form` or `query` — how the login fields travel. Default `json` |
81
+ | `apiKeyHeader` | The header an `apiKey` credential is sent in. Default `x-api-key` |
82
+ | `tokenPath` | Required. Dot-path to the token in the login response |
83
+ | `expiresAtPath` | Dot-path to an epoch-seconds expiry. Defaults to the JWT's `exp` when claims come from the JWT |
84
+ | `identity` | An operation that returns the signed-in user, called with the new token — for logins that return only a token |
85
+ | `claims.source` | `jwt`, `response` or `identity`. Default `identity` when `identity` is set, else `response` |
86
+ | `claims.*` | Dot-paths to `externalId`, `email`, `name` (one path or several joined with a space), `role`, `tenantId` |
87
+ | `emailTemplate` | Builds an email for an upstream user who has none: `{login}`, `{externalId}`, `{host}`. Such an address never claims an existing user |
88
+ | `roles` | Maps the raw `claims.role` value onto an app role; an unmapped value gets no role |
89
+
90
+ Check it before building on it: start `pikku dev`, sign in once with
91
+ `POST /api/auth/sign-in/delegated` as a real upstream user, and call an exposed
92
+ operation with the session. A wrong password must be refused. Keep real
93
+ credentials in the environment, never in the config or a test.
94
+
95
+ What the install wires for delegated mode — `pikkuDelegatedAuth` in
96
+ `src/auth.ts`, the token stored per user, actors carrying it into scenarios — is
97
+ in the `pikku-auth` skill's `references/better-auth.md`.
98
+
99
+ ## The screens
100
+
101
+ **If the app has a UI, build the sign-in or connect screen that matches the chosen auth mode (Sign in with <X> for delegated, a Connect <X> screen for per-user keys/OAuth, nothing for a shared secret), labelling fields in the upstream's terms (e.g. 'Dolibarr login', not 'Email').**
102
+
103
+ - **Delegated** — the sign-in page posts to `POST /api/auth/sign-in/delegated`
104
+ with the fields named in `credentials` (`login` or `username`, `password`).
105
+ It replaces email sign-up; there is no "create account".
106
+ - **Per-user key or OAuth** — a Connect screen after sign-in, and wherever a
107
+ call fails with `missing_credential`.
108
+ - A `credential_rejected` error (`CredentialRejectedError`, 403) means the
109
+ upstream refused the stored token: show the sign-in again (`reauth:
110
+ 'sign-in'`) or the Connect screen (`reauth: 'connect'`), not a generic error.
111
+
112
+ ## Scenarios
113
+
114
+ A persona that signs in through the upstream needs an upstream credential to
115
+ act with. The install adds `credentials` to `pikkuActor` in `src/auth.ts`, and
116
+ at sign-in each actor stores `ACTOR_CREDENTIAL_<PERSONA>_<NAME>` from the
117
+ environment (e.g. `ACTOR_CREDENTIAL_SALES_REP_DOLIBARR`). Without it every
118
+ scenario step that reaches the upstream fails with `missing_credential`. The
119
+ `pikku-scenario` skill's `references/personas.md` has the details.
@@ -16,6 +16,10 @@ failed; a showcase where every surface is a stub has also failed. The bar for
16
16
  each surface below: **it does something the app genuinely needs, and a scenario
17
17
  proves it.** A cron job that logs "tick" is not a schedule — it is a comment.
18
18
 
19
+ Each surface lands with its console link, filtered by the person's level
20
+ (SKILL.md, "Who you are talking to"): a non-technical person sees the workflow
21
+ or agent page, never the queue, scheduler or wire pages behind it.
22
+
19
23
  Budget the extra surfaces at one milestone each. They are not free, and a
20
24
  half-wired workflow engine is worse than no workflow engine.
21
25
 
@@ -223,9 +223,10 @@ bunx --bun pikku scenario run local --spawn
223
223
 
224
224
  ## 6. Hand it over honestly
225
225
 
226
- Tell the user, in one short paragraph: what runs, what it is seeded with, and
227
- that this is a quick build — no knowledge base, no milestones, no design pass,
228
- access control clicked-through rather than proven.
226
+ Tell the user, in one short paragraph and at their level (SKILL.md, "Who you
227
+ are talking to"), with console links rather than descriptions: what runs, what
228
+ it is seeded with, and that this is a quick build — no knowledge base, no
229
+ milestones, no design pass, access control clicked-through rather than proven.
229
230
 
230
231
  **Upgrading to a real build is additive, not a rewrite.** If they want it, switch
231
232
  to `references/app.md` and do this, in order:
@@ -3,16 +3,15 @@ name: pikku-knowledge
3
3
  description: >-
4
4
  Use when writing, reading, reorganising or validating a project's knowledge/ directory — the
5
5
  notes that say what the app is, in the language its users use. Covers the Open Knowledge Format
6
- note (path-as-identity markdown, YAML frontmatter, only `type` required), the sections of the
7
- app-project profile (slices, entities, decisions, questions, wishlist) and the one question each
8
- answers, slice status/entities/gherkin rules, the `resource:` URI scheme tying a note to the
9
- code it is about, the shapes that are NOT a knowledge base, and the `pikku knowledge
10
- validate|index` commands. TRIGGER when: user asks to write down a decision, requirement, entity
11
- or open question; asks what the app does or is; asks about knowledge/, notes, slices,
12
- an index.md, or a diagram, callout or decision block; or hands over a product
13
- brief to record. DO NOT TRIGGER when: user asks what
14
- functions, routes, tables or permissions exist (that is `pikku meta` / `pikku info`, never a
15
- note), or to write a scenario test (use pikku-scenario).
6
+ note (path-as-identity markdown, YAML frontmatter, only `type` required), the app-project
7
+ profile's sections (milestones, entities, decisions, questions, wishlist) and what each answers,
8
+ milestone status/entities/gherkin rules, the `resource:` URI scheme tying a note to its code,
9
+ what is NOT a knowledge base, and `pikku knowledge validate|index`. TRIGGER when: user asks to
10
+ write down a decision, requirement, entity or open question; asks what the app does or is; asks
11
+ about knowledge/, notes, milestones, an index.md, or a diagram, callout or decision block; or
12
+ hands over a product brief to record. DO NOT TRIGGER when: user asks what functions, routes,
13
+ tables or permissions exist (that is `pikku meta` / `pikku info`, never a note), or to write a
14
+ scenario test (use pikku-scenario).
16
15
  installGroups: [core]
17
16
  agent:
18
17
  tools: read, write, edit, bash, grep
@@ -76,7 +75,7 @@ Frontmatter fields:
76
75
 
77
76
  | Field | Meaning |
78
77
  | ------------- | ----------------------------------------------------------------------------------------------------------------------- |
79
- | `type` | **The only required field.** `slice` — or `milestone`, when the project's own `knowledge/index.md` names the section that way; `validate` accepts both, so follow the scaffold rather than this list. Then `entity`, `decision`, `note`, `overview`. Lowercase — gates compare it literally. |
78
+ | `type` | **The only required field.** One of `milestone`, `entity`, `decision`, `note`, `overview`. Lowercase — every gate compares it literally, and `milestone` is the exact string `readMilestones` filters on, so a near-miss is a note no command can see. |
80
79
  | `title` | What to call the note in a listing. Falls back to the first heading, then the filename. |
81
80
  | `description` | One line, used as the note's subtitle in a section index. |
82
81
  | `resource` | Comma-separated `<kind>:<id>` URIs — the code this note is about. See below. |
@@ -92,9 +91,9 @@ Plain markdown links between notes — `[revocation](../decisions/revocation-end
92
91
  ```
93
92
  knowledge/
94
93
  index.md # type: overview — the map
95
- slices/
94
+ milestones/
96
95
  index.md
97
- 01-the-daily-entry.md # type: slice
96
+ 01-the-daily-entry.md # type: milestone
98
97
  entities/
99
98
  index.md
100
99
  entry.md # type: entity
@@ -116,7 +115,7 @@ Each section answers exactly one question, which is what lets a reader find a no
116
115
 
117
116
  | Section | The question it answers |
118
117
  | --------------------- | ------------------------------------------------------------------ |
119
- | `slices/` | What is one buildable piece of this app, and what proves it works? |
118
+ | `milestones/` | What is one buildable piece of this app, and what proves it works? |
120
119
  | `entities/` | What is this thing, in the words users use for it? |
121
120
  | `decisions/` | What was chosen, and what does that rule out? |
122
121
  | `decisions/security/` | Who may do what? |
@@ -125,13 +124,13 @@ Each section answers exactly one question, which is what lets a reader find a no
125
124
 
126
125
  **Create a section the turn you have a note for it** — never a scaffold of empty directories, and never a section without its own `index.md`. A section index says in one line what belongs in it; that sentence is the reason the file exists, so `pikku knowledge index` writes only the note listing and leaves your prose alone.
127
126
 
128
- ## Slices
127
+ ## Milestones
129
128
 
130
- A slice is the one note type that is a piece of _work_ rather than a fact, so it alone carries state and size:
129
+ A milestone is the one note type that is a piece of _work_ rather than a fact, so it alone carries state and size. It lives in `knowledge/milestones/` and nowhere else: `readMilestones` matches on that directory literally, so the same note under another section is invisible to every gate and command below.
131
130
 
132
131
  ````markdown
133
132
  ---
134
- type: slice
133
+ type: milestone
135
134
  title: The daily entry
136
135
  description: An owner writes one entry per day, and sees it on the day.
137
136
  status: proposed
@@ -151,9 +150,9 @@ And writing again replaces it rather than adding a second
151
150
  ```
152
151
  ````
153
152
 
154
- - **`status`** is `designing` → `proposed` → `dispatched` → `built`. Nothing else. Every gate compares it literally. `designing` sits BEFORE `proposed`: the slice is written down but must not be built yet, because whoever is being shown its looks has not picked one. Only `proposed` is dispatchable, so the two cannot be one status without a slice being built out from under the person still choosing.
153
+ - **`status`** is `proposed` → `dispatched` → `built`. Nothing else — `MILESTONE_STATUSES` is those three, and every gate compares them literally, so an invented status fails `validate` rather than degrading. Only `proposed` is dispatchable. A profile may add one of its own ahead of `proposed` for work that is written down but must not be built yet; that is the profile's to define and validate, not core's.
155
154
  - **`statusAt:` and `attempts:` are bookkeeping, not content — never hand-edit them.** A loop driving this base writes both. `statusAt:` is stamped by whatever moved the status, and is what makes "how long has this been building?" answerable; the file's mtime is not the transition time, because a note is edited after dispatch for all sorts of reasons. `attempts:` is `seat@hash` entries recording which seat has already tried to move this note forward, against the content it was trying to move — it is the loop's only brake, and clearing it by hand hands back a budget that exists to stop a note nothing can satisfy being rewritten forever. Rewriting the note's real content refunds that budget on its own, which is the point: an answer that changes the note is what unsticks it.
156
- - **`entities`** lists what the slice touches, **at most three**. Past three it is not one buildable piece — split it.
155
+ - **`entities`** lists what the milestone touches, **at most three**. Past three it is not one buildable piece — split it.
157
156
  - **The scenario is a fenced `gherkin` block, in the third person.** `Given 'owner' has no entry` — never `Given I have no entry`. A quoted word _means a persona_, which is what lets a reader (and a test) tell who is acting. First person hides that, so it is rejected. The console draws the keywords as a column and each quoted persona as a chip, so a first-person scenario is visibly a block with no personas in it.
158
157
 
159
158
  ## Showing it
@@ -231,7 +230,7 @@ These are all things that exist somewhere better, so a note is always the copy t
231
230
  | Do not write | Because it lives in |
232
231
  | ----------------------------------- | ---------------------------------------------------------- |
233
232
  | a `personas/` section | `definePersonas()` in the project's own code |
234
- | a `scenarios/` section | the gherkin block inside the slice it belongs to |
233
+ | a `scenarios/` section | the gherkin block inside the milestone it belongs to |
235
234
  | a `permissions/` section | a decision note under `decisions/security/` |
236
235
  | a list of tables, columns or routes | `pikku meta` — the generated schema _is_ the schema |
237
236
  | a changelog | `CHANGELOG.md` at the repo root |
@@ -250,7 +249,7 @@ pikku knowledge index # refresh every index.md
250
249
  pikku knowledge index --check # report stale indexes without writing (CI gate)
251
250
  ```
252
251
 
253
- `validate` reports: notes with no `type`, a missing `knowledge/index.md`, a section with no `index.md`, notes flat at the root, sections that duplicate what the project already declares, slices with a bad or missing `status`, slices over three entities, slices with no gherkin block or a first-person one, `decision` fences that state no `chosen:` or rule nothing out, and every `resource:` that no longer resolves. Errors fail the command; warnings do not.
252
+ `validate` reports: notes with no `type`, a missing `knowledge/index.md`, a section with no `index.md`, notes flat at the root, sections that duplicate what the project already declares, milestones with a bad or missing `status`, milestones over three entities, milestones with no gherkin block or a first-person one, `decision` fences that state no `chosen:` or rule nothing out, and every `resource:` that no longer resolves. Errors fail the command; warnings do not.
254
253
 
255
254
  `index` rewrites only the block between `<!-- pikku:knowledge-index -->` markers, creating a scaffolded `index.md` for a section that has none. It is idempotent — running it twice changes nothing.
256
255
 
@@ -312,4 +311,4 @@ The exception, and it is narrow: bookkeeping the loop owns. `statusAt:` and `att
312
311
 
313
312
  OKF permits frontmatter fields a reader does not know, and the parser ignores them rather than failing. That is the extension point: a tool layered on Pikku can add its own sections and fields on top of everything above without forking the format.
314
313
 
315
- Fabric is the one that exists. It adds `decisions/design/` — rules about how the app looks and behaves — and a `design:` field on a slice pointing at the design options it was built from. Both are Fabric's to validate; `pikku knowledge validate` passes them through untouched. Everything else in this skill is the same in both.
314
+ Fabric is the one that exists. It adds `decisions/design/` — rules about how the app looks and behaves — a `screens/` section, and a `design:` field on a milestone pointing at the design options it was built from. Both are Fabric's to validate; `pikku knowledge validate` passes them through untouched. Everything else in this skill is the same in both.
@@ -35,13 +35,13 @@ Use this skill as an execution checklist, not reference material.
35
35
  This skill covers writing and running scenarios end to end. Four topics are one level down, and
36
36
  each says when to open it:
37
37
 
38
- | Read | For |
39
- | --------------------------- | ------------------------------------------------------------------------- |
40
- | `references/steps.md` | Authoring a `pikkuScenarioStep` — intent, witnesses, what a step is given |
41
- | `references/browser.md` | Browser bindings, and locating by message key in a translated app |
42
- | `references/personas.md` | Personas vs actors, `definePersonas`, and the human "Sign in as …" switcher |
43
- | `references/coverage.md` | Live coverage, filling it, and unit tests for pure logic |
44
- | `references/persona-run.md` | Running a persona as a model-driven virtual user against a stage |
38
+ | Read | For |
39
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------ |
40
+ | `references/steps.md` | Authoring a `pikkuScenarioStep` — intent, witnesses, what a step is given |
41
+ | `references/browser.md` | Browser bindings, and locating by message key in a translated app |
42
+ | `references/personas.md` | Personas vs actors, `definePersonas`, upstream credentials for actors, and the human "Sign in as …" switcher |
43
+ | `references/coverage.md` | Live coverage, filling it, and unit tests for pure logic |
44
+ | `references/persona-run.md` | Running a persona as a model-driven virtual user against a stage |
45
45
 
46
46
  ## What a scenario is
47
47
 
@@ -245,7 +245,7 @@ suite survives its first redesign:
245
245
  through the actor.
246
246
  - **Steps describe intent, not actions** — `buys the £5 strawberry milkshake`, never
247
247
  `clicks [data-testid=add]`. The clicking lives in plain utilities the step composes.
248
- - **`then` bindings are witnesses, not alternatives** — a `then` runs *every* binding it declares
248
+ - **`then` bindings are witnesses, not alternatives** — a `then` runs _every_ binding it declares
249
249
  and fails when they disagree, because "database right, user still watching a spinner" is the bug
250
250
  nobody catches.
251
251
  - **What language the prose is in** — identifiers are English in every project; `description`,
@@ -265,6 +265,7 @@ whole project — codegen builds the `PersonaId` union from it, materialises one
265
265
  scenario actor per person, and seeds a user row each:
266
266
 
267
267
  ```ts snippet:definePersonas
268
+
268
269
  ```
269
270
 
270
271
  `pikku.config.json` carries the settings around them — nothing about a person:
@@ -5,7 +5,7 @@ A **persona** is a person your product is for; an **actor** is one body that sig
5
5
  - Two people of the same kind are two entries, not one persona with two logins — "you see yours, not theirs" is only testable with two customers.
6
6
  - Never write an email address: each is derived from the persona id and `scenarios.emailDomain`, and a hand-written one signs in as somebody who was never created.
7
7
  - `roles` is typechecked against `defineSystemRole`; an undeclared role is a build error.
8
- - A person who is only ever acted *upon* — the account an admin bans — sets `runnable: false`: declared and seeded, never signed in, because a run as them would race the scenario that acts on them.
8
+ - A person who is only ever acted _upon_ — the account an admin bans — sets `runnable: false`: declared and seeded, never signed in, because a run as them would race the scenario that acts on them.
9
9
  - A persona holds only what is true of that kind of person for the app's whole lifetime (`name`, `jobTitle`, `description`, `personality`, `roles`, `goals`, `disposition`). What someone is trying to get done, and the circumstances they are doing it in, belong to the **scenario**, not to them.
10
10
 
11
11
  ## Declaring personas in TypeScript
@@ -50,6 +50,32 @@ A project that never declares a persona keeps working: a scenario that names no
50
50
  - `environments.<name>.apiUrl` is required. `signInPath` defaults to `/auth/sign-in/actor`, `rpcPath` to `/rpc`.
51
51
  - **`SCENARIO_ACTOR_SECRET` is an environment variable and never goes in `pikku.config.json`.** It signs actors in. `pikku scenario run` throws without it; a server auto-building actors warns and runs without them.
52
52
 
53
+ ## Actors that call a third-party API
54
+
55
+ An addon generated from an upstream API (Dolibarr, a CRM, a calendar) calls it
56
+ with the signed-in user's own credential. An actor has none until one is stored
57
+ for it, so every step that reaches the addon fails with `No <X> session`. Give
58
+ the `actor` plugin a `credentials` option, and each actor carries its upstream
59
+ credential into every session:
60
+
61
+ - `names: ['dolibarr']` — the credentials to carry
62
+ - `store: (name, value, userId) => credentialService.set(name, value, userId)`
63
+ - `remove: (name, userId) => credentialService.delete(name, userId)`
64
+
65
+ At each actor sign-in the plugin reads `ACTOR_CREDENTIAL_<PERSONA>_<NAME>` —
66
+ `dan` + `dolibarr` is `ACTOR_CREDENTIAL_DAN_DOLIBARR` — and stores it with
67
+ `credentialService.set` for that actor. A bare value is stored as `{ token }`
68
+ (what a delegated or bearer credential holds); a JSON object is stored as-is,
69
+ e.g. `{"apiKey":"…"}` for an API-key credential. Unset means the actor has no
70
+ upstream credential: `remove` drops one stored at an earlier sign-in.
71
+
72
+ - **Values live in `.env` (or CI secrets), never in `personas.ts` or code.**
73
+ Use a dedicated upstream test account per persona, not a real person's.
74
+ - `read` defaults to `process.env`; a Worker passes `(key) => variables.get(key)`.
75
+ - A malformed JSON value refuses the sign-in with a 500 that names the variable.
76
+ - A delegated token expires upstream like any other; re-signing the actor
77
+ in re-stores whatever the variable holds now.
78
+
53
79
  ## The same actors sign a human in
54
80
 
55
81
  Declared actors are not only for automated runs. `signInPath` is Better Auth's
@@ -57,8 +83,8 @@ Declared actors are not only for automated runs. `signInPath` is Better Auth's
57
83
  frontend gets a one-click "Sign in as …" switcher over the **same** list, and an
58
84
  app can be reviewed as each kind of user without anyone knowing a seed password.
59
85
 
60
- The sandbox dev server bakes both halves into the frontend from the declared
61
- personas: `VITE_DEV_ACTORS` (the JSON actor list) and `VITE_DEV_ACTOR_SECRETS`
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`
62
88
  (`{ email: credential }`, one per persona — `SCENARIO_ACTOR_SECRET` itself never
63
89
  goes in a bundle; see **pikku-auth**). Neither var is set in a production
64
90
  build, so the control renders nothing there — but gate the reads on your
@@ -85,3 +111,39 @@ When the switcher is there but signing in fails with `401 Invalid actor
85
111
  secret`, check which server answered before checking the secret: a frontend
86
112
  whose dev proxy (`VITE_API_PROXY`, default `http://localhost:3000`) points at
87
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.