@pikku/skills 0.12.43 → 0.12.46

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.
@@ -8,8 +8,8 @@ the last phase, and nothing in it is needed before then.
8
8
  `pikku deploy` builds and ships without any hosted service:
9
9
 
10
10
  ```sh
11
- bunx --bun pikku deploy plan --provider standalone --runtime bun
12
- bunx --bun pikku deploy apply --provider standalone --runtime bun
11
+ bunx --bun pikku deploy plan --provider standalone
12
+ bunx --bun pikku deploy apply --provider standalone
13
13
  ```
14
14
 
15
15
  `standalone` comes from the installed `@pikku/deploy-standalone` adapter: it
@@ -108,6 +108,14 @@ workspace). Serve each behind its own hostname, and put the API behind `/api` on
108
108
  prefix, everything else under `/api/*` reaches the pikku server unprefixed. Get
109
109
  this wrong and sign-in fails on one app only, which is a miserable thing to debug.
110
110
 
111
+ To serve one frontend from the pikku server's own origin instead — one binary,
112
+ one hostname, first-party cookies — give its `frontends` entry `"serve": {}`
113
+ (`urlPrefix` defaults to `/`, `spaFallback` to true). `pikku serve`, `pikku dev`
114
+ and a standalone deploy then mount its built `dist`. Pikku never builds it, so
115
+ build the frontend first. Only one entry may set `serve`.
116
+
117
+ To ship an app as a desktop or Android app, read `references/native-app.md`.
118
+
111
119
  Before shipping, run the full gate:
112
120
 
113
121
  ```sh
@@ -129,7 +137,7 @@ out of.** The server-side pass proves the functions; it renders nothing. The
129
137
  pages are client-rendered, so a component that throws still returns HTTP 200
130
138
  with an empty shell — the same trap §6 warns about, and the release gate is
131
139
  exactly where it gets shipped past. Run the browser pass, and run it **for every
132
- environment in `pikkufabric.config.json`**, not just the first:
140
+ environment in `pikku.config.json`**, not just the first:
133
141
 
134
142
  ```sh
135
143
  bunx --bun pikku scenario run local --spawn --run browser
@@ -150,11 +158,13 @@ every save — but run it.
150
158
  Everything above is open source. This is the contract that keeps
151
159
  `pikku fabric init` a one-command import later, instead of a migration.
152
160
 
153
- - **`pikkufabric.config.json` describes reality.** Every app has an entry with
154
- the right `cwd`, `port`, `kind` and `dev.command`; exactly one is `primary`;
155
- `serves` and `personas` name real personas from the personas section. Leave `projectId` as
156
- `__PROJECT_ID__` — that placeholder means "unlinked", and linking the project
157
- writes the real one. Do not invent a value to make it look configured.
161
+ - **`frontends` in `pikku.config.json` describes reality.** Every app has an
162
+ entry with the right `cwd`, `kind` and `dev` (`command`, `port`); exactly one
163
+ is `primary`; `serves` and `personas` name real personas from the personas
164
+ section.
165
+ - **`fabric.projectId` in `pikku.config.json` is optional.** Leave it out unless you
166
+ have the real id; the CLI writes it after finding the project from the git
167
+ remote. Do not invent a value.
158
168
  - **One `definePersonas` call**, every persona reachable through exactly one
159
169
  frontend. Fabric materialises these as its virtual users; a persona nobody
160
170
  serves imports as a person with no way in.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pikku-changes
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.'
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|reply|shot|done`: waiting for work without polling, asking instead of guessing, saying why an item is left undone, 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 checkout linked to a Fabric project (`pikku fabric config` shows one). 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
 
@@ -10,7 +10,7 @@ Someone walked the deployed app and circled things. Each item is their words, a
10
10
  screenshot of what they saw, and the elements the circle enclosed. You have the repo.
11
11
  Empty the queue without making them regret filing.
12
12
 
13
- Run every command from the checkout: the project comes from `pikkufabric.config.json`.
13
+ Run every command from the checkout: the project comes from its git remote (`pikku fabric config` shows which).
14
14
  `--json` works on all of them. Items are addressed as `2`, `#2` or their uuid.
15
15
 
16
16
  ## Which stage
@@ -33,19 +33,21 @@ something changed. `next` does the waiting and exits only when there is work.
33
33
  It waits out the grace window (a just-filed item is held about a minute so a batch
34
34
  being typed arrives together), claims what is ready as one group, prints it, and
35
35
  exits. It also wakes when someone answers a question you asked under that
36
- `--claimed-by`.
36
+ `--claimed-by`. It is woken by fabric's change events the moment an item is filed
37
+ or answered, and sleeps exactly until a held item becomes claimable; when the event
38
+ stream is unavailable it falls back to checking every `--interval` seconds.
37
39
 
38
40
  2. When it exits, read the exit code:
39
41
 
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 |
42
+ | code | meaning | do |
43
+ | ---- | ------------------------------------------------------ | ----------------------------------------------- |
44
+ | 0 | work printed (claimed, and/or `Answered`) | work it, then step 3 |
45
+ | 2 | `--timeout`/`--once` found nothing | stop, or restart `next` |
46
+ | 3 | session refused | tell the user to run `pikku fabric login`; stop |
47
+ | 1 | anything else (bad `--stage`, fabric down for minutes) | report the message; stop |
46
48
 
47
- 3. For each item: `show` → fix → commit → `done`, or `ask` and move on. Then start
48
- `next` again, in the background.
49
+ 3. For each item: `show` → fix → commit → `done`; or `ask` and move on; or `reply`
50
+ saying why you are leaving it. Then start `next` again, in the background.
49
51
 
50
52
  Without `--claim` it only reports what is claimable; claim it yourself:
51
53
 
@@ -53,8 +55,10 @@ Without `--claim` it only reports what is claimable; claim it yourself:
53
55
  pikku fabric changes claim --change-ids 3,4 --title "Checkout pass" --claimed-by claude-code
54
56
  ```
55
57
 
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
+ A `claim` refused with a 409 says per item why — held for the filer, inside another
59
+ group's lease, done — and when a held or leased item is **claimable at** (a local
60
+ `HH:MM`; `list` and `show` print the same). An item inside someone else's live lease
61
+ cannot be taken. For held items, run `next --claim` rather than retrying. The lease is 30 minutes
58
62
  (`--lease-minutes`); an abandoned claim returns to the queue by itself.
59
63
 
60
64
  ## Reading an item
@@ -67,7 +71,7 @@ For held items, run `next --claim` rather than retrying. The lease is 30 minutes
67
71
  3. **The circled elements**: a testid (greps straight to a component, it is the i18n
68
72
  key), a source anchor, a CSS path.
69
73
  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.
74
+ was at _that_ commit. Find today's equivalent; never edit line 42 because it said 42.
71
75
 
72
76
  ## When to ask
73
77
 
@@ -97,6 +101,24 @@ pikku fabric changes shot --change-id 3 --label "Bigger" --kind option --image a
97
101
 
98
102
  `--kind evidence` is a picture that proves something, shown inline.
99
103
 
104
+ ## Replying without asking
105
+
106
+ `ask` is for a decision you need from them: it parks the item as needing an answer.
107
+ `done` closes it. Everything else you have to say goes in a `reply`, which leaves the
108
+ item's status exactly where it was:
109
+
110
+ - you are not doing it, and why ("the copy comes from the CMS, not the app");
111
+ - it is blocked on something that is not a question ("needs STRIPE_KEY set on the stage");
112
+ - you could not reproduce it — attach what you saw.
113
+
114
+ ```bash
115
+ pikku fabric changes reply 3 --message "Cannot reproduce on develop @ a91c4e2 — this is what I see." \
116
+ --image seen.png --image-label "develop @ a91c4e2" --author-name claude-code
117
+ ```
118
+
119
+ Never `ask` a question you do not need answered just to leave a note, and never
120
+ `done --note` an item you did not do — both tell the filer the wrong thing.
121
+
100
122
  ## Committing
101
123
 
102
124
  One item, one commit — `done` records one sha, and that is what a human reverts. The
@@ -117,7 +139,7 @@ pikku fabric changes done --change-id 7 --note "What you did, for whoever reads
117
139
  ```
118
140
 
119
141
  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
142
+ An item you decided not to do is not `done`: `reply` with why and leave it for a
121
143
  human to dismiss.
122
144
 
123
145
  Writes need the `changes:project:write` scope; `list`, `show` and `next` without
@@ -8,10 +8,9 @@ There are two ways to start a Pikku app. Pick based on whether you need to own t
8
8
 
9
9
  ```typescript
10
10
  // src/lifecycle.ts
11
- import { pikkuServerLifecycle } from '@pikku/core'
12
- import type { SingletonServices } from '../types/application-types.js'
11
+ import { pikkuServerLifecycle } from '#pikku/setup'
13
12
 
14
- export const lifecycle = pikkuServerLifecycle<SingletonServices>({
13
+ export const lifecycle = pikkuServerLifecycle({
15
14
  beforeStart: async ({ kysely }) => {
16
15
  await runMigrations(kysely)
17
16
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pikku-fabric
3
- description: 'Build, convert and debug apps on the Pikku Fabric platform. Covers SQLite/libSQL database setup with Kysely, fabric project layout, deploy provider config, `pikkufabric.config.json`, the `pikku all` + `tsc` verification loop, and reading logs, traces and metrics from a deployed stage. TRIGGER when: user is working on a Fabric-hosted Pikku project, converting an app to Fabric format, asking about Fabric deployment, database or project conventions, asking about a `pikku fabric validate` finding including app-missing-actor-quick-login, or a deployed stage is erroring, timing out or behaving differently than local ("why is prod failing", "check the logs"). DO NOT TRIGGER when: user is working on a generic (non-Fabric) Pikku deployment — use pikku-deploy instead — or the failure reproduces locally, which is where to debug it.'
3
+ description: 'Build, convert and debug apps on the Pikku Fabric platform. Covers SQLite/libSQL database setup with Kysely, fabric project layout, deploy provider config, the optional `fabric.projectId` in `pikku.config.json`, the `pikku all` + `tsc` verification loop, and reading logs, traces and metrics from a deployed stage. TRIGGER when: user is working on a Fabric-hosted Pikku project, converting an app to Fabric format, asking about Fabric deployment, database or project conventions, asking about a `pikku fabric validate` finding including app-missing-actor-quick-login, or a deployed stage is erroring, timing out or behaving differently than local ("why is prod failing", "check the logs"). DO NOT TRIGGER when: user is working on a generic (non-Fabric) Pikku deployment — use pikku-deploy instead — or the failure reproduces locally, which is where to debug it.'
4
4
  installGroups: [fabric]
5
5
  ---
6
6
 
@@ -91,6 +91,31 @@ Numbers must be consecutive and gap-free, and an applied migration is frozen —
91
91
  correct a mistake with a new forward migration, never by editing or renaming one
92
92
  that has already run (the recorded hash will no longer match).
93
93
 
94
+ **Forward-only rule.** Once a migration exists on the base branch (or any stage
95
+ has applied it), never edit, rename or delete it — add a NEW numbered migration
96
+ that makes the change. A stage records a migration by name and never re-runs it,
97
+ so an edit never reaches a database that already applied it. Two guards enforce
98
+ this:
99
+
100
+ - `pikku fabric validate` reports `migration-modified-after-base-*` (error) for
101
+ any `db/<engine>/*.sql` that exists on the base ref (default `origin/main`, else `main`, `origin/master`, `master`,
102
+ compared at the branch's merge-base; override with `--migrations-base <ref>`
103
+ or `PIKKU_MIGRATIONS_BASE`) but was modified, deleted or renamed in the working
104
+ tree. New files are fine. It is skipped outside a git repo or with no base ref.
105
+ In CI use a full clone (`fetch-depth: 0`) so the base ref exists.
106
+ - `pikku fabric deploy apply` runs the migration-history checks first and
107
+ refuses to create a deployment if any fail. It compares against the stage
108
+ being deployed and the production (`main`) stage's applied ledger, and against
109
+ the base ref — a branch stage can be reset at will, but main's history reaches
110
+ production. Findings: `migration-applied-file-missing-*`, `migration-drift-*`,
111
+ `migration-gap`, `migration-modified-after-base-*`. Unlike `validate`, a check
112
+ that cannot run refuses too: an unreadable ledger (`migration-drift-unchecked`)
113
+ or a base ref that does not resolve (`migration-base-unresolved`). There is no
114
+ override flag — fix the history.
115
+
116
+ Fix a finding by restoring the file (`git checkout origin/main -- db/sqlite/<file>`)
117
+ and putting the change in a new migration.
118
+
94
119
  Run migrations: `pikku db migrate`. It also regenerates `.pikku/db/schema.gen.ts`
95
120
  (Kysely types) and `.pikku/db/zod.gen.ts` — there is no separate types step.
96
121
 
@@ -111,8 +136,10 @@ to be a migration instead.
111
136
 
112
137
  This is **local dev data only**: enough rows that a fresh dev database isn't an
113
138
  empty app. Nothing else ever runs it. A deployed stage applies `db/<engine>/*.sql`
114
- and stops there — reset refuses `NODE_ENV=production` and refuses a database
115
- outside the runtime directory, and no deploy step reaches for the seed file.
139
+ and stops there — the one exception is a disposable stage deployed with
140
+ `pikku fabric deploy apply <branch> --reset` (see Deploy), which is never
141
+ production. Local reset refuses `NODE_ENV=production` and refuses a database
142
+ outside the runtime directory.
116
143
 
117
144
  So the test is not "is this row realistic?", it is **"would the app be broken
118
145
  without it in production?"** If yes, it is configuration and belongs in a
@@ -186,47 +213,70 @@ packages/functions/
186
213
  apps/app/ # Frontend(s)
187
214
  db/sqlite/ # Plain .sql migrations, numbered, gap-free (project root)
188
215
  db/sqlite-dev-seed.sql # Dev-only test data, applied by `pikku db reset`
189
- pikku.config.json # Pikku + deploy config (project root)
190
- pikkufabric.config.json # Fabric project link + frontends (project root)
216
+ pikku.config.json # Pikku + deploy config, and the frontends (project root)
191
217
  ```
192
218
 
193
- ## `pikkufabric.config.json`
219
+ ## How a checkout is linked to its Fabric project
220
+
221
+ `fabric.projectId` in `pikku.config.json` names the project. It is optional. Order
222
+ tried:
223
+
224
+ 1. `FABRIC_PROJECT_ID` env var — CI and scripts.
225
+ 2. `fabric.projectId` in `pikku.config.json`.
226
+ 3. The git remote, matched against Fabric's projects. The id found is written
227
+ into `pikku.config.json` so the lookup happens once. It is never committed
228
+ for you; commit it or not. A deploy ignores a `pikku.config.json` whose only
229
+ change is `fabric.projectId`. If two projects share one repo the CLI refuses; pick
230
+ one with `FABRIC_PROJECT_ID=<projectId>`.
231
+
232
+ `pikku fabric config` prints which project, api url and login the current
233
+ checkout resolves to, and where each came from, and the project's settings.
234
+ The settings Fabric needs that `pikku.config.json` does not describe are stored
235
+ on the project, not in a file, and set with `key=value` arguments:
194
236
 
195
- Links the repo to a Fabric project and declares its frontends:
237
+ ```bash
238
+ pikku fabric config showcase.name="Watering Log" showcase.tags=voice,realtime
239
+ pikku fabric config guide.docs=docs/guide guide.theme.primaryColor=teal
240
+ pikku fabric config scenarios.env.STRIPE_MODE=test # the scenario run only
241
+ pikku fabric config showcase.tint= # an empty value clears it
242
+ ```
243
+
244
+ The keys are `showcase.name` (≤60), `showcase.description` (≤160),
245
+ `showcase.tags` (comma-separated, ≤6, from Fabric's fixed list),
246
+ `showcase.tint` (`#rrggbb`), `guide.docs`, `guide.theme.primaryColor`,
247
+ `guide.theme.fontFamily`, `guide.theme.headingFontFamily` and
248
+ `scenarios.env.<NAME>`. Every assignment is checked before any is applied, so a
249
+ refused one changes nothing. Whether the project appears on the public
250
+ showcase is Fabric's decision, not a setting.
251
+
252
+ `pikku fabric link` creates
253
+ `origin` (with `--gitea`) if there is none, imports the project, writes
254
+ `projectId`, and queues the first deploy; it commits and pushes nothing. A
255
+ custom production domain is set with `pikku fabric domains add`. Production
256
+ always maps to `main`; without a domain it lives on the platform
257
+ `*.pikkufabric.app` hostnames.
258
+
259
+ The apps are the `frontends` in `pikku.config.json`,
260
+ the same list `pikku serve`, `pikku app` and native builds read, and
261
+ `pikku fabric validate` and `smoke` read them from there:
196
262
 
197
263
  ```json
198
- {
199
- "projectId": "my-project-id",
200
- "production": {
201
- "domain": "example.com"
202
- },
203
- "frontends": {
204
- "app": {
205
- "cwd": "apps/app",
206
- "primary": true,
207
- "deploy": true,
208
- "kind": "ssr",
209
- "dev": {
210
- "command": ["yarn", "dev"],
211
- "port": 7105,
212
- "healthPath": "/"
213
- }
264
+ "frontends": {
265
+ "app": {
266
+ "cwd": "apps/app",
267
+ "primary": true,
268
+ "deploy": true,
269
+ "kind": "ssr",
270
+ "dev": {
271
+ "command": ["yarn", "dev"],
272
+ "port": 7105,
273
+ "healthPath": "/"
214
274
  }
215
275
  }
216
276
  }
217
277
  ```
218
278
 
219
- - `projectId`: written by `pikku fabric init` / `link`. Templates ship the
220
- `__PROJECT_ID__` placeholder — that is _not_ a link, and the CLI treats it as
221
- unlinked.
222
- - `production.domain`: optional custom domain. Production always maps to `main`;
223
- without a domain it lives on the platform `*.pikkufabric.app` hostnames.
224
- - `frontends`: each entry declares a frontend app with its dev command and port
225
-
226
- Several CLI messages call this file `fabric.config.json` — `fabric init --force`,
227
- `fabric link --apiUrl`, and the `domains` commands' "No fabric.config.json found".
228
- The file the CLI actually reads and writes is `pikkufabric.config.json`; don't
229
- create the shorter name to satisfy an error message.
279
+ Each `frontends` entry declares a frontend app with its dev command and port.
230
280
 
231
281
  ## RPC is the default transport
232
282
 
@@ -352,6 +402,24 @@ pikku fabric deploy apply my-branch -y # a named one
352
402
  that drop or rewrite data — that stays `--allow-destructive`, typed out on
353
403
  purpose.
354
404
 
405
+ ### Rebuilding a disposable stage: `--reset`
406
+
407
+ A non-production stage whose migration history or schema has drifted (an app's
408
+ `develop` branch) can be wiped and rebuilt in one deploy:
409
+
410
+ ```bash
411
+ pikku fabric deploy apply develop --reset # asks first, naming app + stage
412
+ pikku fabric deploy apply develop --reset -y # prints the warning, skips the prompt
413
+ ```
414
+
415
+ It is the deployed counterpart of `pikku db reset`: **all data on that stage is
416
+ deleted**, every migration is re-applied and `db/<engine>-dev-seed.sql` is
417
+ loaded. It is refused for `--production`, for `main`, and with `--deployment-id`
418
+ (it only applies to a deploy it creates). It needs a fabric server that reports
419
+ the reset back on the deployment; against one that does not, the command fails
420
+ naming the deployment it created without a reset, and wipes nothing. `-y` does
421
+ not imply `--allow-destructive`.
422
+
355
423
  Inferring the branch is safe because the git safety check refuses any branch
356
424
  without an upstream or out of sync with it, so it cannot ship an unpushed
357
425
  commit; the branch it picked is printed before the build starts. A detached
@@ -430,8 +498,8 @@ repo, and if it is installed with "selected repositories" this one must be in
430
498
  the selection. There is no CLI flag that works around a missing installation:
431
499
  `init` returns "Connect the GitHub account '<owner>'". Send the user to install
432
500
  it, or create the project in the console instead (which provisions a Fabric-hosted
433
- git repo you push to) and write the returned `projectId` into
434
- `pikkufabric.config.json` yourself.
501
+ git repo you push to) and clone it — the remote links the checkout, or put
502
+ the id in `fabric.projectId` in `pikku.config.json`.
435
503
 
436
504
  Deploy refuses to run unless the target branch equals its upstream — the guard
437
505
  compares `main` against `main@{upstream}`. So the remote you pushed to must be
@@ -477,17 +545,16 @@ reviewer has no seed password, so without the control they are locked out of the
477
545
  app they were asked to look at.
478
546
 
479
547
  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.
548
+ own UI built on `useDevActors()` or `signInAsPersona()` from `@pikku/react` —
549
+ validate accepts any of those call sites as evidence, so custom rendering
550
+ passes. Either way the server needs `personaSignIn` on `pikkuActor`; see
551
+ **pikku-scenario** for it and **pikku-react** for the props.
483
552
 
484
553
  The validator also accepts the shapes that predate the package — a hand-rolled
485
554
  `signInAsActor()` or a literal `POST /auth/sign-in/actor` — so an older app does
486
555
  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.
556
+ to `<DevActorSwitcher />`.** They put a per-persona credential in the frontend
557
+ bundle, which the persona endpoint exists to avoid.
491
558
 
492
559
  Do **not** satisfy it with Better Auth's `/dev/quick-login`. That is a different
493
560
  endpoint with a different purpose — one fixed admin, not the declared personas —
@@ -521,7 +588,7 @@ Fix every `error` and `warn` in the output before continuing. Then:
521
588
  3. **Replace DI/IoC with pikkuServices**: move service construction to `createSingletonServices` in `services.ts`.
522
589
  4. **Replace `process.env` calls**: plain config becomes `defineVariable` + `variables.get()`, anything sensitive becomes `defineSecret` + `secrets.getSecret()`.
523
590
  5. **Add `pikku.config.json`** at project root with `srcDirectories`, `outDir`, and `clientFiles` — plus `metaLocale` if the team does not work in English, which is the language every `description`, `title` and step `template` is then authored in.
524
- 6. **Add `pikkufabric.config.json`** at project root with `projectId`, `production.domain`, and `frontends` (production is always `main`, so there is no `production.branch`).
591
+ 6. **Declare the apps** as `frontends` in `pikku.config.json`. There is no fabric config file; `projectId` is optional; without it the CLI finds the project from the git remote.
525
592
  7. **Run `pikku all`** — verify codegen succeeds and there are no type errors.
526
593
  8. **Run `pikku fabric validate`** once more to confirm no structural issues remain.
527
594
 
@@ -374,6 +374,7 @@ a log line or an audit row. See `pikku-services` for the reveal rules.
374
374
  import {
375
375
  PikkuKysely,
376
376
  PgKyselyChannelStore,
377
+ PgKyselyLeaseService,
377
378
  PgKyselyWorkflowService,
378
379
  } from '@pikku/kysely-postgres'
379
380
 
@@ -385,10 +386,22 @@ const createSingletonServices = pikkuServices(async (config) => {
385
386
  const channelStore = new PgKyselyChannelStore(db.kysely)
386
387
  await channelStore.init()
387
388
 
388
- const workflowService = new PgKyselyWorkflowService(db.kysely)
389
+ const leaseService = new PgKyselyLeaseService(db.kysely)
390
+ await leaseService.init()
391
+
392
+ const workflowService = new PgKyselyWorkflowService(db.kysely, {
393
+ leaseService,
394
+ })
389
395
  await workflowService.init()
390
396
 
391
- return { config, logger, database: db, channelStore, workflowService }
397
+ return {
398
+ config,
399
+ logger,
400
+ database: db,
401
+ channelStore,
402
+ leaseService,
403
+ workflowService,
404
+ }
392
405
  })
393
406
  ```
394
407
 
@@ -409,8 +422,20 @@ await channelStore.init()
409
422
  ### MySQL Setup
410
423
 
411
424
  ```typescript
412
- import { MySQLKyselyWorkflowService } from '@pikku/kysely-mysql'
425
+ import {
426
+ MySQLKyselyLeaseService,
427
+ MySQLKyselyWorkflowService,
428
+ } from '@pikku/kysely-mysql'
429
+
430
+ const leaseService = new MySQLKyselyLeaseService(kyselyInstance)
431
+ await leaseService.init()
413
432
 
414
- const workflowService = new MySQLKyselyWorkflowService(kyselyInstance)
433
+ const workflowService = new MySQLKyselyWorkflowService(kyselyInstance, {
434
+ leaseService,
435
+ })
415
436
  await workflowService.init()
416
437
  ```
438
+
439
+ Every persistent workflow service takes a required `leaseService` and locks runs
440
+ and steps on it. Pass the same instance you register as the app's
441
+ `leaseService`.
@@ -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`.
@@ -46,6 +46,7 @@ behind the interface.
46
46
  | `PikkuWorkflowService`, `WorkflowRunService` | Redis, MongoDB | `@pikku/redis`, `@pikku/mongodb` |
47
47
  | `SessionStore`, `AgentRunService`, `DeploymentService` | Redis, MongoDB | `@pikku/redis`, `@pikku/mongodb` |
48
48
  | `AgentStorageService`, `AgentRunStateService` | MongoDB **only** | `@pikku/mongodb` |
49
+ | `LeaseService` | `RedisLeaseService` | `@pikku/redis` |
49
50
 
50
51
  SQL is the third option for every store interface in that table —
51
52
  `KyselyChannelStore`, `KyselyWorkflowService`, `KyselySecretService` and friends