@pikku/skills 0.12.41 → 0.12.43

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,292 @@
1
+ # Scenarios — writing journeys that stay proven
2
+
3
+ A scenario is a user journey run as one of your personas, over the real transport, with that
4
+ persona's session. It is the only kind of test worth writing here, because a passing one proves the
5
+ app works the way a signed-in person experiences it.
6
+
7
+ The traps below all cost a real milestone a red run, and most of them are invisible on the run that
8
+ introduces them. Read the section that matches what you are writing.
9
+
10
+ 1. [The shape of a scenario](#the-shape-of-a-scenario)
11
+ 2. [Extraction: what survives, and what silently does not](#extraction-what-survives-and-what-silently-does-not)
12
+ 3. [What to assert](#what-to-assert)
13
+ 4. [There is no state reset](#there-is-no-state-reset)
14
+ 5. [Steps rot as the app grows](#steps-rot-as-the-app-grows)
15
+ 6. [Browser scenarios](#browser-scenarios)
16
+ 7. [Running them](#running-them)
17
+ 8. [Coverage — which functions have actually been run](#coverage--which-functions-have-actually-been-run)
18
+
19
+ ---
20
+
21
+ ## The shape of a scenario
22
+
23
+ Three ship in `packages/functions/test/scenarios/` — keep them green — and every milestone's gherkin
24
+ block from §5 becomes one more.
25
+
26
+ ```typescript
27
+ import { pikkuScenario } from '#pikku/scenarios'
28
+
29
+ export const tenantReportsAFaultScenario = pikkuScenario<void, { id: string }>({
30
+ title: 'A tenant reports a fault and the owner sees it',
31
+ description: 'The report lands on the owning landlord’s queue, and nobody else’s',
32
+ tags: ['scenario', 'maintenance'],
33
+ func: async (_services, _data, { scenario, actors }) => {
34
+ const report = await scenario.do(
35
+ 'reports a broken boiler',
36
+ 'createMaintenanceReport',
37
+ { summary: 'No hot water' },
38
+ { actor: actors.chidi },
39
+ )
40
+ await scenario.then(
41
+ 'appears on the owner’s queue',
42
+ 'reportShowsOnQueue',
43
+ { id: report.id },
44
+ { actor: actors.amina },
45
+ )
46
+ await scenario.then(
47
+ 'is invisible to the other owner',
48
+ 'reportIsNotVisible',
49
+ { id: report.id },
50
+ { actor: actors.bilal },
51
+ )
52
+ return { id: report.id }
53
+ },
54
+ })
55
+ ```
56
+
57
+ **`do` takes an RPC name; `given`/`when`/`then` take a declared step.** A step is a
58
+ `pikkuScenarioStep` that says what a person is doing and holds one implementation per surface
59
+ (server-side by default, plus a `browser` one that drives the page). Reaching for an RPC name in a
60
+ `then` will not resolve.
61
+
62
+ **`SCENARIO_ACTOR_SECRET` must be in `.env`** (app.md §6, before the first run). Without it
63
+ `/api/auth/sign-in/actor` is disabled — every scenario then fails at sign-in, before its first step,
64
+ for a reason that reads like an auth bug. `pikku scenario run` reads it from the environment, so
65
+ source `.env` first (`set -a && . ./.env && set +a`) when you run outside `bun run dev`.
66
+
67
+ ---
68
+
69
+ ## Extraction: what survives, and what silently does not
70
+
71
+ A scenario body is extracted as a DSL workflow, so it is not ordinary TypeScript. The failures here
72
+ are the expensive kind: two of the three produce a GREEN suite that proves the wrong thing.
73
+
74
+ **Every scenario must assert, and `return await scenario.then(...)` does not count.** A ladder of
75
+ `given`/`when` with no `then` is a PKU680 critical — it fails `pikku all`, so it stops codegen
76
+ rather than a test. Coverage counts every step, so without that rule an assertion-free ladder of
77
+ clicks would score a perfect run while checking nothing. The extractor reads the body statically and
78
+ does not see a `then` in `return` position: seven refusal scenarios here, each ending
79
+ `return await scenario.then('is refused …', …)`, were all reported as never asserting. Bind it —
80
+ `const asserted = await scenario.then(...)`, then `return asserted` on the next line — which is also
81
+ how the value stays inspectable when the step's output is what the scenario returns.
82
+
83
+ **Only `const`/`let`, `if`/`else`, `switch`, `for..of`, `return`, `throw` and workflow calls
84
+ survive.** A counting `for` is refused by PKU679, and so is a `for..of` whose iterable is written
85
+ inline — it must be a named identifier or a field (`data.items`). Bind the seat numbers, the ids,
86
+ the rows to a `const` above the loop and iterate that. One milestone here lost a codegen round to
87
+ each half of that rule, because the first half does not imply the second.
88
+
89
+ **Write each scenario's setup out in full rather than sharing a local helper.** A helper holding
90
+ setup steps does not fail extraction — the steps are recorded and the suite goes green — but the
91
+ extractor cannot bind an `actor` that arrives as a function parameter, so the transcript credits
92
+ every setup step to whoever the last literal binding named. Seven permission scenarios here read
93
+ "henrik sets up her company Salon Nordlicht" in a suite whose whole point was that the company is
94
+ finja's. A scenario body is a recorded document, and the duplication is the price of it saying who
95
+ did what.
96
+
97
+ ---
98
+
99
+ ## What to assert
100
+
101
+ **Write the refusals, and assert WHY.** One persona reaching for another's row has to be rejected,
102
+ and that rejection is a scenario — it is how you prove access control instead of asserting it. But
103
+ only if the step reads the reason: "not ok" is also what a malformed request returns, so a refusal
104
+ step that stops at the status code passes on a call the function never even ran. One here posted
105
+ straight to `/rpc/<name>` with the function's input as the body; that route validates an ENVELOPE
106
+ (`{ rpcName, data }`), so the input read as a bag of unknown properties and came back 422. Asserting
107
+ the refusal MENTIONED the rule — a company, an owner, a scope — turned a green false positive into a
108
+ one-line fix.
109
+
110
+ **Assert totals as deltas.** A screen that counts or sums every row — a revenue tile, a queue count,
111
+ a dashboard — is reporting the whole history of a database nobody resets. Read the summary before
112
+ the journey, read it after, and assert what the journey moved. An absolute ("the failed count is
113
+ zero") is a claim about every run that came before. And when a tile turns out to be one no journey
114
+ in the app can move at all, that is a finding about the app, not an assertion to force: assert it
115
+ held still, say why in the step's doc comment, and tell the user.
116
+
117
+ **Green twice is not the same as unchanged twice — count the rows.** A save that appends where it
118
+ should replace passes every assertion while doubling a table. One here re-sent a product's variants
119
+ without their ids, so the addon read each as new: 1, 2, 4, 8, and by the nineteenth save 262,144
120
+ rows, every run green until the request crossed a body-size limit and surfaced as a `413` that read
121
+ like an infrastructure fault. The assertion nobody writes is the count, and it is one SQL query.
122
+
123
+ **One screen's extra field does not belong on the shared output schema.** A detail page almost
124
+ always wants one column the list does not — when the licence was handed over, who last touched the
125
+ row. Extending the shared `XDetail` that four functions already return bumps the contract of all
126
+ four, for a field three of them never render. Extend at the new function's own output instead —
127
+ `XDetail.extend({ assignedAt })`, named for the screen that asked. Say in the new type's doc comment
128
+ WHY it is not on the base, or the next build merges them back.
129
+
130
+ ---
131
+
132
+ ## There is no state reset
133
+
134
+ A scenario runs against a live server: scope what you create to your own rows and unique ids, and
135
+ never assume a clean database. A scenario that leaves durable state changed has to put it back — the
136
+ one that archives a product unarchives it, the one that cancels a plan restarts it — because its own
137
+ second run starts where its first one stopped.
138
+
139
+ **Run the suite twice and require the second run green.** A suite that only passes on a fresh
140
+ database is a suite that passes once. Two corollaries, both of which cost a milestone a red run:
141
+
142
+ - **Name nothing a setup step might already own.** `setsUpHerCompany` returns the company that actor
143
+ already has rather than renaming it, so a later step passed the literal name it had asked for and
144
+ was told no such company exists. Read the name, slug or id back off the step's output and pass
145
+ THAT.
146
+ - **Put rows somewhere the other scenarios are not.** An import seeded at the same coordinates as
147
+ another scenario's salons accumulated one row per run until it crowded that scenario's own salon
148
+ out of a nearest-N list. Anything a scenario asserts by proximity, recency or a top-N cut is
149
+ asserting against every row every previous run left behind.
150
+
151
+ **Moving the clock forward runs every rule between here and there.** A step that time-travels so a
152
+ scheduled job will fire is not asking for that job — it is asking for all of them, in order. One
153
+ swept to 2030 to reach a four-week chase and found the pile it was about to assert on empty, because
154
+ an unreturned pen reactivates a membership sixty days after cancellation and the sweep had walked
155
+ straight past that window. Travel to the day the rule under test fires and no further, computed off
156
+ a date the scenario read back (`daysAfter(periodEnd, 1)`), never to a round far-future date — and
157
+ when a sweep surprises you, the next rule in the calendar is the first place to look.
158
+
159
+ ---
160
+
161
+ ## Steps rot as the app grows
162
+
163
+ A step is shared, so it is the one thing in the suite that a milestone which never mentions it can
164
+ break. Whatever a step selects by, ask what ELSE will match it after the suite has run a hundred
165
+ times.
166
+
167
+ **Drive a new step from both sides in the milestone that adds it.** A step is only as proven as its
168
+ best-exercised branch: one here read the wrong field off a raw invocation (`attempt.data`; the
169
+ payload is `attempt.body`), so its found-case could never pass — invisible for as long as every
170
+ caller asked for ABSENCE.
171
+
172
+ **When you add a writer of a row an existing step selects by recency, give that step an explicit
173
+ filter in the same change.** "The newest X" stops meaning "the one this scenario just made" the
174
+ moment a second function produces X: a renewal job that raised invoices broke three invoice
175
+ scenarios that had been green for months.
176
+
177
+ **A selector on IDENTITY alone rots the same way once rows gain a lifecycle.** One reused the first
178
+ licence assigned to an email regardless of its state, which was correct until a new milestone's
179
+ refusal scenarios left that actor holding cancelled ones — it then handed back a dead licence, read
180
+ as success, and failed a scenario two milestones older at a step that needed a live one.
181
+
182
+ **A step's input is a recorded contract, and it does not take a version.** Widening one — an extra
183
+ optional field so a step can name a particular row — trips PKU861 exactly like a function's does,
184
+ but the fix that works for a function does not work here: adding `version: 2` to a
185
+ `pikkuScenarioStep` makes the runner unable to find the step at all, and every scenario using it
186
+ fails with `Function not found`. Add a NEW step beside the old one. That is the better answer
187
+ anyway, because a step that has grown an optional field is usually two questions wearing one name —
188
+ "does she have an invoice like this" and "what became of the invoice I am holding" — and the
189
+ scenarios read better once they say which one they are asking.
190
+
191
+ ---
192
+
193
+ ## Browser scenarios
194
+
195
+ **A click returns before its effect lands — assert the effect, then navigate.** A browser step's
196
+ click resolves when the button was pressed, not when the mutation it fired came back. So a step that
197
+ presses "Add to cart" and the next one that opens `/app/cart` are in a race with the `onSuccess`
198
+ that writes the cart token to `localStorage`, and the loser arrives at an empty basket. It passed on
199
+ the first run here and failed on the second, which is the worst way to find out. Put an assertion on
200
+ the confirmation between them — the button's own "Added", the toast, the row that appeared — so the
201
+ navigation waits on the write instead of on luck. This is also why a browser scenario that is only
202
+ clicks reads better than it tests: every `when` that writes wants a `then` before the next page.
203
+
204
+ **A control the browser cannot NAME is a control it cannot drive.** A testid is derived from a
205
+ message key at build time, which has two consequences that only show up when a scenario tries to
206
+ press something:
207
+
208
+ - A label chosen at runtime — `label={isCancel ? m.a() : m.b()}` — derives no key at all, so the
209
+ field is unreachable and the failure reads as a missing element rather than a conditional. Write
210
+ the two controls out separately, each with its own static call.
211
+ - Every row of a list carries the SAME keys, so `pause` on a list of twelve licenses is twelve
212
+ matches and a strict-mode violation. Scoping by text does not save it either, because a button's
213
+ own text is "Pause" and not the row's. Give the row an address of its own —
214
+ `data-testid={id.slice(0, 8)}` on the card, rendered beside the title so a person can read it too
215
+ — and address the control `within` it.
216
+
217
+ Both are screen defects before they are test defects: a field whose label changes identity under it,
218
+ and a list whose rows are indistinguishable to anyone on the phone to support.
219
+
220
+ **A route nested under an existing screen is unreachable until its parent renders an `Outlet`.** A
221
+ milestone added `/app/academy/$slug` under an `/app/academy` that already had a component of its
222
+ own; the parent swallowed the child, so the editor's URL rendered the list — every link, every route
223
+ file and every type check looked right, and the only thing that noticed was a browser scenario that
224
+ OPENED the child path and found the parent's controls on screen. When a milestone deepens a path an
225
+ earlier one already owns, split the parent into a layout (`Outlet`) and an `index` route in the same
226
+ change, and make one scenario open the child by path rather than reach it by clicking.
227
+
228
+ **Never hard-code the target's origin.** A raw-HTTP step — a webhook, a check-in a scanner posts —
229
+ runs in the CLI process, not on the server, so it has to be told where to post. That is
230
+ `wire.scenarioStep.env.apiUrl`, which carries the environment and whatever `--api-url` overrode it.
231
+ A literal `http://localhost:3000` does not merely break on another port: it silently posts into
232
+ whatever else is listening there, so the scenario goes green having never touched this app at all.
233
+
234
+ ---
235
+
236
+ ## Running them
237
+
238
+ ```sh
239
+ bunx --bun pikku scenario run local --spawn # server-side, the fast path
240
+ bunx --bun pikku scenario run local --spawn --run browser # the same journeys, driven as a human
241
+ ```
242
+
243
+ In a multi-app project that one run covers both frontends: each persona carries
244
+ its own `app` and `@pikku/playwright` resolves the base url from the
245
+ environment's `appUrls` map, so there is no second environment to run.
246
+
247
+ `--spawn` starts and stops the server for the run; drop it if `bun run dev` is already up. The
248
+ browser pass needs the environment's `appUrl` and a browser driver installed — without them the run
249
+ fails fast rather than half-running.
250
+
251
+ **Run the whole suite, not the milestone's own scenarios.** The milestone's scenarios are the ones
252
+ you wrote to pass; the regression lives in someone else's. Tightening what "archived" means is a
253
+ one-function change that reads as local and quietly breaks the milestone-01 scenario nobody re-ran.
254
+
255
+ **Restart the server after adding a function.** Hot reload does not register a new RPC and does not
256
+ re-run `afterStart`, so a fresh function answers 404 and anything provisioned at boot is missing —
257
+ failures that read like a wiring bug and are nothing but a stale process.
258
+
259
+ ---
260
+
261
+ ## Coverage — which functions have actually been run
262
+
263
+ Green scenarios tell you the journeys you wrote still work. They say nothing about the code you
264
+ never wrote a journey for, and that gap is invisible without measuring it:
265
+
266
+ ```sh
267
+ bunx --bun pikku dev --coverage # server, instrumented
268
+ bunx --bun pikku scenario run local --coverage # against that server
269
+ ```
270
+
271
+ That writes `coverage/scenario-coverage.json` — which functions each journey exercised. **A function
272
+ no scenario touches has never been run by anything but you, by hand, once.** It compiles, it
273
+ typechecks, `pikku all` is happy, and nobody has proven it does what it says.
274
+
275
+ Run it **as each milestone closes**, not once at the end. Coverage read per milestone is a short
276
+ list you can act on — the milestone you just built either covered its own functions or it did not.
277
+ Read for the first time after ten milestones it is a wall of red that nobody triages, and the honest
278
+ response to a wall of red is to ignore it.
279
+
280
+ Every gap is one of three things, and naming which is the point of looking:
281
+
282
+ - **A missing scenario** — the function matters and no journey reaches it. Write the journey.
283
+ Refusal paths dominate this category, because it is the case you are least likely to have clicked
284
+ through by hand.
285
+ - **A function that should not exist** — nothing reaches it because nothing needs it. Delete it. An
286
+ unused exposed function is also reachable over `POST /rpc/:rpcName`, so this is a security finding,
287
+ not only dead weight.
288
+ - **Genuinely deferred** — real, not yet reachable from the UI. Say so in the milestone note that
289
+ will cover it, so the gap is a decision rather than a hole.
290
+
291
+ Report the number when you hand the milestone over. A number nobody says out loud is a number nobody
292
+ acts on.
@@ -132,7 +132,7 @@ exactly where it gets shipped past. Run the browser pass, and run it **for every
132
132
  environment in `pikkufabric.config.json`**, not just the first:
133
133
 
134
134
  ```sh
135
- bunx --bun pikku scenario run local-admin --spawn --run browser
135
+ bunx --bun pikku scenario run local --spawn --run browser
136
136
  ```
137
137
 
138
138
  `bun run build` is what type-checks each frontend (each app's `tsc` script runs
@@ -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.
@@ -376,6 +376,12 @@ returns 0, which tells you nothing about whether it worked:
376
376
  | 3 | the deployment is blocked and nothing the CLI can do will unblock it |
377
377
  | 4 | the wait hit `--timeout` with the deployment still in flight |
378
378
 
379
+ On a failure or timeout, `apply` prints the tail of the builder's own log (and
380
+ carries `buildLog` / `imageBuildLog` on the `--json` result). Read it before
381
+ touching code: `fabric logs` serves the running stage, not the build. When the
382
+ builder recorded nothing, the CLI says so — that is usually fabric-side, so run
383
+ `pikku fabric smoke` before assuming the project is broken.
384
+
379
385
  Fabric parks every deploy at a gate after the plan phase (`status: suspended`).
380
386
  Why it parked is the whole story, and it is `statusReason`, not `status`:
381
387
 
@@ -433,6 +439,21 @@ the one the branch tracks; a stale `origin` left over from scaffolding blocks
433
439
  the deploy with "local HEAD … ≠ remote …" even though your code is pushed.
434
440
  `git branch --set-upstream-to=<remote>/main main` before deploying.
435
441
 
442
+ ### The first user on a deployed stage
443
+
444
+ When sign-up is off, the first account has to come from outside the app.
445
+ `pikku fabric user add <email>` is the CLI form of the console's Add user:
446
+
447
+ ```bash
448
+ pikku fabric user add ada@example.com --name Ada # prompts; blank generates one
449
+ pikku fabric user add ada@example.com --password '<pw>' -b staging
450
+ ```
451
+
452
+ It mints a short-lived operator token for the stage and calls the stage's own
453
+ `admin:createUser`, so the stage must wire `@pikku/addon-admin` as `admin` —
454
+ a 404 is refused by name and nothing is created. A generated password is printed
455
+ once; one you passed is never echoed. With no TTY, pass `--password` or pipe it.
456
+
436
457
  ## Versioning
437
458
 
438
459
  Functions with `expose: true` are versioned via `versions.pikku.json`. When you change a function's input or output schema, you must bump its version number — otherwise `pikku all` will report a breaking change and callers' generated clients become stale.