@pikku/skills 0.12.33 → 0.12.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.33",
3
+ "version": "0.12.35",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -47,7 +47,7 @@ wireAddon({
47
47
  package: string, // NPM package name (e.g. '@pikku/addon-todos')
48
48
  rpcEndpoint?: string, // Optional remote RPC endpoint for distributed execution
49
49
  auth?: boolean, // Require a session for every function in the addon
50
- mcp?: boolean,
50
+ mcp?: boolean | string[], // true: every function the addon declared mcp: true; a list: the tools this app offers, typed against the addon's function names
51
51
  tags?: string[], // Tags applied to all addon functions
52
52
  scopes?: string[], // Required of every function, on top of its own
53
53
  secretOverrides?: Record<string, string>, // Remap secret names (and grant them)
@@ -324,11 +324,6 @@ wireAddon({ name: 'todos', package: '@my-org/addon-todos' })
324
324
 
325
325
  After registration, run `yarn pikku all` to generate types for the addon's functions.
326
326
 
327
- Give each addon its own wiring file. A deployment unit imports a `wireAddon`
328
- file only while at least one addon that file wires survives the unit's filter,
329
- so wiring two addons from one file means a unit needing either one registers
330
- both and bundles both packages' dependencies.
331
-
332
327
  If the addon ships tables, `pikku db generate` then writes one migration per
333
328
  addon — named after the package, carrying the addon's own SQL — after Better
334
329
  Auth's and the runtime's, so an addon table may reference `user` or a runtime
@@ -3,32 +3,45 @@ name: pikku-architect
3
3
  description: >-
4
4
  Use to turn one settled milestone note into the technical plan the build is measured against —
5
5
  the tables, functions, wires, roles, scopes, screens and scenarios it owes, split into passes and
6
- written through `pikku knowledge plan set`. This is a SEPARATE SEAT from the build: the plan is
7
- the denominator `pikku knowledge plan progress` divides by, so whoever writes it must not be the
8
- one grading themselves against it. TRIGGER when: a milestone note is settled and the next step is
6
+ written through `pikku knowledge plan set`. The plan is the denominator
7
+ `pikku knowledge plan progress` divides by, so it is written BEFORE any of the milestone's code
8
+ exists and never edited afterwards to match what got built. TRIGGER when: a milestone note is settled and the next step is
9
9
  planning it, the user asks to plan or architect a milestone, `pikku knowledge plan progress` says
10
10
  a milestone has no plan, or pikku-build's App mode reaches a milestone with nothing planned. DO
11
11
  NOT TRIGGER when: the milestone notes themselves are still being written (use pikku-knowledge),
12
12
  the plan already exists and the job is to build it (use pikku-build), or the ask is a one-off
13
13
  edit to a working app.
14
14
  installGroups: [core]
15
+ agent:
16
+ tools: read, write, edit, bash, grep
17
+ timeoutMs: 1800000
18
+ acceptance:
19
+ level: verified
20
+ evidence: [changed-files, validation-output]
21
+ verify:
22
+ - id: knowledge-consistent
23
+ command: pikku knowledge validate
24
+ - id: plan-accepted
25
+ command: pikku knowledge next --require dispatch,idle
26
+
15
27
  ---
16
28
 
17
29
  # Plan one milestone
18
30
 
19
31
  A milestone note says what the app must DO and how it must feel for the person using it. It
20
- deliberately does not say how. You are the seat that decides how, once, in writing, before anyone
21
- builds it.
32
+ deliberately does not say how. This is where how gets decided, once, in writing, before any of
33
+ it is built.
22
34
 
23
- **Why this is a separate seat.** The build agent used to write its own plan. That makes one party
24
- both author and examiner: it can build a fraction, plan only that fraction, and certify itself
25
- complete — and `pikku knowledge plan progress` then divides by a denominator the builder chose
26
- after seeing its own answer. A plan written here, against the note, by someone who is not going to
27
- build it, is the denominator the builder does not own.
35
+ **Why the plan comes first and stays fixed.** A builder who plans after seeing its own work can
36
+ build a fraction, plan only that fraction, and certify itself complete — `pikku knowledge plan
37
+ progress` then divides by a denominator chosen after the answer. The defence is the ORDER: the plan
38
+ is written against the note, in its own turn, before a single migration for it exists, and is never
39
+ edited afterwards. An item that will not land is deferred with its reason through `plan defer`,
40
+ not rewritten out of the plan. The same agent plans and then builds; nothing hands off.
28
41
 
29
- **One milestone, one plan, then stop.** Do not build in this session. Do not plan the next
30
- milestone "while you are here" — the notes after this one are still allowed to change, and a plan
31
- written against a note that later moves is worse than no plan.
42
+ **One milestone, one plan, then build it.** Do not plan the next milestone "while you are here" —
43
+ the notes after this one are still allowed to change, and a plan written against a note that later
44
+ moves is worse than no plan.
32
45
 
33
46
  ---
34
47
 
@@ -38,6 +51,11 @@ The plan reaches disk through `pikku knowledge plan set <milestone> <file>` and
38
51
  validates first and names the field that is wrong if it refuses; a plan file written with an editor
39
52
  is a plan nothing checked, and the place that discovers that is a finished build.
40
53
 
54
+ It is also written ONCE. `plan set` refuses a milestone that already has a plan: the plan is the
55
+ order the build is measured against, and an order that can be rewritten measures nothing. The one
56
+ way down from a written plan is `pikku knowledge plan defer <milestone> <item> --reason <why>`,
57
+ which records what was left out and why.
58
+
41
59
  It is JSON rather than a note on purpose. Everything else under `knowledge/` is prose a human
42
60
  reads; this one is consumed field-by-field, and a markdown parser is one more place a misspelt
43
61
  heading silently passes. It cannot live INSIDE the milestone note either: that note is frozen once
@@ -260,6 +278,7 @@ cover it — so a role × resource cross product there costs the milestone nothi
260
278
 
261
279
  ## When you are done
262
280
 
263
- The accepted `plan set` is the end of the seat. Hand the milestone to `pikku-build`, which reads the
264
- plan with `plan show --for-build`, builds it, and closes the milestone only when
265
- `pikku knowledge plan progress` is clean. What you wrote is what it is measured against.
281
+ The accepted `plan set` is the end of planning. Go straight on to the build in `pikku-build` §6:
282
+ read the plan with `plan show --for-build`, build it, and close the milestone only when
283
+ `pikku knowledge plan progress` is clean. What you wrote is what you are measured against, so do
284
+ not touch it once the first migration is open.
@@ -13,19 +13,31 @@ description: >-
13
13
  allowed-tools: Bash(yarn pikku meta *), Bash(yarn pikku all *), Bash(yarn tsc), Bash(git status *), Bash(git diff *), Bash(git switch *), Bash(git checkout *), Bash(git checkout -b *), Bash(git add *), Bash(git commit *), Bash(git rm *), Bash(git mv *), Bash(git log *), Bash(git branch *), Bash(yarn pikku fabric report *), Bash(npx --no pikku fabric report *)
14
14
  argument-hint: '[feature description]'
15
15
  installGroups: [core]
16
+ agent:
17
+ tools: read, write, edit, bash, grep
18
+ timeoutMs: 5400000
19
+ acceptance:
20
+ level: verified
21
+ evidence: [changed-files, tests-added, commands-run, validation-output]
22
+ verify:
23
+ - id: knowledge-consistent
24
+ command: pikku knowledge validate
25
+ - id: typechecks
26
+ command: pikku all --tsc-summary
27
+
16
28
  ---
17
29
 
18
30
  # Build on Pikku
19
31
 
20
32
  ## Which mode
21
33
 
22
- | The situation | Read |
23
- | --- | --- |
24
- | A template was just cloned or scaffolded, and the tree still looks like one | `references/post-clone.md` first, then come back |
25
- | A real product, meant to be picked up by someone else | `references/app.md` — the default |
26
- | A spike, a throwaway demo, an idea nobody has committed to | `references/quick.md` |
27
- | A showcase meant to exercise every Pikku surface | `references/platform.md`, which is a delta on top of `references/app.md` |
28
- | A feature added to an app that already has its knowledge base and milestones | `references/feature.md` |
34
+ | The situation | Read |
35
+ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
36
+ | A template was just cloned or scaffolded, and the tree still looks like one | `references/post-clone.md` first, then come back |
37
+ | A real product, meant to be picked up by someone else | `references/app.md` — the default |
38
+ | A spike, a throwaway demo, an idea nobody has committed to | `references/quick.md` |
39
+ | A showcase meant to exercise every Pikku surface | `references/platform.md`, which is a delta on top of `references/app.md` |
40
+ | A feature added to an app that already has its knowledge base and milestones | `references/feature.md` |
29
41
 
30
42
  **App is the default.** A small or toy-sounding app does not make it Quick;
31
43
  only an explicit signal of speed or throwaway-ness does. Platform is not "App
@@ -53,9 +65,10 @@ while still planning. Those failures look alarming and are nothing but this.
53
65
 
54
66
  ## What holds in every mode
55
67
 
56
- - **The branch and the diff are the contract.** There is no plan JSON. A
57
- reviewer sees real, compiled, working code: apply is a merge, reject is a
58
- `git branch -D`.
68
+ - **The branch and the diff are the contract.** A reviewer sees real, compiled,
69
+ working code: apply is a merge, reject is a `git branch -D`. The milestone's
70
+ plan is your own denominator, measured by `pikku knowledge plan progress` —
71
+ never something a reviewer is handed instead of the code.
59
72
  - **Discover before editing.** `yarn pikku meta context --json` returns
60
73
  functions, wires, middleware, permissions, workflows, `capabilities` and
61
74
  `layout` in one call. Fall back to targeted `meta` commands only for a full
@@ -66,11 +79,13 @@ while still planning. Those failures look alarming and are nothing but this.
66
79
  lives in `messages/*.json`.
67
80
  - **`pikku all` is the gate.** Run it after touching functions, wirings or
68
81
  schemas, and treat its criticals as real.
69
- - **A milestone is planned by a different seat than the one that builds it.**
82
+ - **A milestone is planned before it is built, and the plan then stays fixed.**
70
83
  The plan — tables, functions, wires, roles, scopes, screens, scenarios, in
71
- passes — is written through `pikku knowledge plan set` by `pikku-architect`,
72
- and `pikku knowledge plan progress` measures the build against it from the
73
- generated meta. A builder who writes its own plan is grading itself.
84
+ passes — is written through `pikku knowledge plan set` (how: `pikku-architect`)
85
+ in its own turn before any of that milestone's code exists, and
86
+ `pikku knowledge plan progress` measures the build against it from the
87
+ generated meta. You plan it and you build it; what you never do is edit the
88
+ plan afterwards to match what you built — that is grading yourself.
74
89
 
75
90
  ## What NOT to do
76
91
 
@@ -81,11 +81,11 @@ A brief saying "the entire UI is German" is about **one** of these. Getting this
81
81
  wrong has already shipped a project that can never add a second language, so
82
82
  settle all three explicitly before you write code.
83
83
 
84
- | Axis | What it covers | Where it goes |
85
- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
86
- | **Identifiers** | Function, component, type, variable and file names. Database tables and columns. Commit messages. | Nowhere — **always English**, no setting, not negotiable |
87
- | **Meta** | `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role and persona descriptions | `metaLocale` in `pikku.config.json`, default `en` |
88
- | **Product UI** | Every string the app shows a user | `messages/<locale>.json`, and `defaultLocale` for what a first-time visitor opens in |
84
+ | Axis | What it covers | Where it goes |
85
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
86
+ | **Identifiers** | Function, component, type, variable and file names. Database tables and columns. Commit messages. | Nowhere — **always English**, no setting, not negotiable |
87
+ | **Meta** | `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role and persona descriptions | `metaLocale` in `pikku.config.json`, default `en` |
88
+ | **Product UI** | Every string the app shows a user | `messages/<locale>.json`, and `defaultLocale` for what a first-time visitor opens in |
89
89
 
90
90
  **Identifiers are English.** The product's market does not change this and
91
91
  neither does `metaLocale`. Identifiers are the surface the generated `#pikku/*`
@@ -218,7 +218,11 @@ defineSystemRole({
218
218
  })
219
219
 
220
220
  definePersonas({
221
- visitor: { name: 'Visitor', jobTitle: 'Synthetic health-check user', account: {} },
221
+ visitor: {
222
+ name: 'Visitor',
223
+ jobTitle: 'Synthetic health-check user',
224
+ account: {},
225
+ },
222
226
  amina: {
223
227
  name: 'Amina',
224
228
  jobTitle: 'Property owner',
@@ -229,7 +233,8 @@ definePersonas({
229
233
  bilal: {
230
234
  name: 'Bilal',
231
235
  jobTitle: 'Property owner',
232
- personality: 'A second owner — exists so "you see yours, not theirs" is testable',
236
+ personality:
237
+ 'A second owner — exists so "you see yours, not theirs" is testable',
233
238
  roles: ['owner'],
234
239
  account: {},
235
240
  },
@@ -369,10 +374,10 @@ scenarios, split into passes. It is JSON, it lives beside the note, and
369
374
  `pikku knowledge plan progress` measures the finished build against it.
370
375
 
371
376
  **Read `pikku-architect` and follow it.** The plan is the denominator the
372
- completion check divides by, so a builder who writes their own plan can build a
373
- fraction, plan only that fraction, and certify itself complete. Fabric answers
374
- that by giving the plan its own seat; here the defence is the ORDER, and it only
375
- holds if you keep it: the plan is written against the note in its own turn,
377
+ completion check divides by, so a builder who plans after seeing their own work
378
+ can build a fraction, plan only that fraction, and certify itself complete. The
379
+ defence is the ORDER, and it only holds if you keep it: the plan is written
380
+ against the note in its own turn,
376
381
  before any of the code it measures exists, and is never edited afterwards to
377
382
  match what you ended up building. An item that will not land is deferred with
378
383
  its reason — `plan defer` — not quietly rewritten. Write it before you open a
@@ -396,8 +401,8 @@ no plan, and everything after the current milestone is still allowed to move.
396
401
 
397
402
  **Per milestone** — plan it (§5a), set its note to `status: dispatched`, do the
398
403
  six steps, close it out (§6a), set it to `built`. Do not start the next one
399
- until §6a passes, §7 is green for this one *and §7a shows its functions
400
- covered*. A stack of half-milestones cannot be reviewed and cannot be handed
404
+ until §6a passes, §7 is green for this one _and §7a shows its functions
405
+ covered_. A stack of half-milestones cannot be reviewed and cannot be handed
401
406
  over, and an uncovered function is a half-milestone whether or not the note says
402
407
  `built`.
403
408
 
@@ -416,7 +421,7 @@ over, and an uncovered function is a half-milestone whether or not the note says
416
421
  Do this generously and do it now: an empty app demos badly and critiques
417
422
  badly, and you cannot judge a screen's hierarchy, overflow, or truncation
418
423
  against zero rows. Seed rows each persona sees differently — with an ownership
419
- rule that means seeding rows for the *second* owner too.
424
+ rule that means seeding rows for the _second_ owner too.
420
425
  3. **Functions.** One `pikkuFunc` per `*.function.ts`. Mark it `expose: true` and
421
426
  Pikku generates the typed RPC client and the React Query hooks the UI calls;
422
427
  you do NOT write an HTTP route for it. Add `wireHTTP` only for a real REST
@@ -443,8 +448,10 @@ over, and an uncovered function is a half-milestone whether or not the note says
443
448
  same as the scenario: a milestone whose screens nobody has seen is not built,
444
449
  it is unproven at the one layer scenarios cannot reach. `references/design.md`
445
450
  carries how to take the shot when no browser tool is wired up, and what to
446
- look for. Then `status: built`, and say in the note what you looked at and
447
- what it made you change.
451
+ look for. Then close it against its plan (§6a) — `pikku knowledge plan
452
+ progress` has to exit zero before anything is `built` — and only then set
453
+ `status: built`, saying in the note what you looked at and what it made you
454
+ change.
448
455
 
449
456
  Rules that are not optional:
450
457
 
@@ -464,8 +471,8 @@ Rules that are not optional:
464
471
  ```typescript
465
472
  export const classifications = {
466
473
  payment: {
467
- paid_at: { kind: 'date' }, // -> Date, not an ISO string
468
- metadata: { kind: 'json', tsType: 'PaymentMeta' }, // -> parsed object, not unknown
474
+ paid_at: { kind: 'date' }, // -> Date, not an ISO string
475
+ metadata: { kind: 'json', tsType: 'PaymentMeta' }, // -> parsed object, not unknown
469
476
  },
470
477
  }
471
478
  ```
@@ -474,6 +481,7 @@ Rules that are not optional:
474
481
  `JSON` column with no entry types as `unknown` (the CLI warns PKU481). Add the
475
482
  annotation rather than casting around the generated type. Once the file carries
476
483
  manual fields, `db migrate` stops overwriting it.
484
+
477
485
  - A `z.date()` on a function's **input** arrives over RPC as an ISO string, not a
478
486
  `Date`. Normalise before calling date methods on it (`new Date(value)`), or it
479
487
  throws `.getTime is not a function` at runtime — schema validation accepts the
@@ -542,6 +550,7 @@ Three things it says, and what each one asks of you:
542
550
  milestone is two milestones — say so to the user rather than deferring again.
543
551
  What you may never do is drop the item silently: the plan is what the next
544
552
  person reads to know what this milestone was for.
553
+
545
554
  - **PROBLEMS** — something exists but does not do what was planned. A function
546
555
  planned as restricted whose meta says `auth: false`; a `cascade` no migration
547
556
  declares; a browser scenario that opens a page and asserts it is still on it.
@@ -550,8 +559,9 @@ Three things it says, and what each one asks of you:
550
559
  visible, never blocking.
551
560
 
552
561
  **Do not set the note to `built` while this exits non-zero**, and do not edit the
553
- plan to match what you built — `plan set` is the architect's seat, and a builder
554
- rewriting its own denominator is exactly what the split exists to stop.
562
+ plan to match what you built — the plan was written before the code on purpose,
563
+ and rewriting your own denominator afterwards is exactly what that order exists
564
+ to stop.
555
565
 
556
566
  ## 7. Prove it — scenarios
557
567
 
@@ -566,26 +576,27 @@ import { pikkuScenario } from '#pikku/scenarios'
566
576
 
567
577
  export const tenantReportsAFaultScenario = pikkuScenario<void, { id: string }>({
568
578
  title: 'A tenant reports a fault and the owner sees it',
569
- description: 'The report lands on the owning landlord’s queue, and nobody else’s',
579
+ description:
580
+ 'The report lands on the owning landlord’s queue, and nobody else’s',
570
581
  tags: ['scenario', 'maintenance'],
571
582
  func: async (_services, _data, { scenario, actors }) => {
572
583
  const report = await scenario.do(
573
584
  'reports a broken boiler',
574
585
  'createMaintenanceReport',
575
586
  { summary: 'No hot water' },
576
- { actor: actors.chidi },
587
+ { actor: actors.chidi }
577
588
  )
578
589
  await scenario.then(
579
590
  'appears on the owner’s queue',
580
591
  'reportShowsOnQueue',
581
592
  { id: report.id },
582
- { actor: actors.amina },
593
+ { actor: actors.amina }
583
594
  )
584
595
  await scenario.then(
585
596
  'is invisible to the other owner',
586
597
  'reportIsNotVisible',
587
598
  { id: report.id },
588
- { actor: actors.bilal },
599
+ { actor: actors.bilal }
589
600
  )
590
601
  return { id: report.id }
591
602
  },
@@ -327,6 +327,19 @@ pikku fabric validate # must pass clean
327
327
  pikku fabric deploy apply --production -y
328
328
  ```
329
329
 
330
+ `init` and `link` import into whichever organization your session is in. When
331
+ you belong to several — a personal one and a company one, say — name the target
332
+ with `--organization`, taking a slug, a display name or an id:
333
+
334
+ ```bash
335
+ pikku fabric link --organization vlandor
336
+ ```
337
+
338
+ You have to be a member of the organization you name, and its GitHub account
339
+ has to be connected already: importing a `github.com/<owner>/<repo>` repo needs
340
+ the Fabric GitHub App installed on `<owner>` _and_ linked to that organization,
341
+ or the import refuses by name.
342
+
330
343
  The branch is positional and defaults to the checked-out one, and `-y` is the
331
344
  short form of `--auto-approve`, so a one-shot deploy is:
332
345
 
@@ -14,6 +14,16 @@ description: >-
14
14
  functions, routes, tables or permissions exist (that is `pikku meta` / `pikku info`, never a
15
15
  note), or to write a scenario test (use pikku-scenario).
16
16
  installGroups: [core]
17
+ agent:
18
+ tools: read, write, edit, bash, grep
19
+ timeoutMs: 1800000
20
+ acceptance:
21
+ level: verified
22
+ evidence: [changed-files, validation-output]
23
+ verify:
24
+ - id: knowledge-consistent
25
+ command: pikku knowledge validate
26
+
17
27
  ---
18
28
 
19
29
  # Pikku Knowledge
@@ -266,7 +276,7 @@ Two things about the output matter if you are driving it:
266
276
  `options` is empty when the answer is free text, and an empty list means offer free
267
277
  text — never invent choices to fill it.
268
278
 
269
- `hold` means a profile's own gate is holding the milestone and no seat this loop knows
279
+ `hold` means a profile's own gate is holding the milestone and nothing this loop knows
270
280
  about can clear it. It names the hold and the notes it is about; what to do then
271
281
  belongs to that profile, not here.
272
282
 
@@ -282,7 +292,7 @@ pikku knowledge plan progress <milestone> # what it still owes, read fr
282
292
  pikku knowledge plan defer <milestone> <item> -r "<why>"
283
293
  ```
284
294
 
285
- `progress` reconciles the plan against pikku's generated meta — set membership, never anyone's status — and exits non-zero while the first pass is short, or while anything already built contradicts the plan. Unbuilt work in a later pass is reported, not blocked; a function that shipped wide open against a planned permission rule blocks from any pass, because that is a hole rather than a backlog. Writing a plan is its own seat: read `pikku-architect`. Building against one is `pikku-build`.
295
+ `progress` reconciles the plan against pikku's generated meta — set membership, never anyone's status — and exits non-zero while the first pass is short, or while anything already built contradicts the plan. Unbuilt work in a later pass is reported, not blocked; a function that shipped wide open against a planned permission rule blocks from any pass, because that is a hole rather than a backlog. How a plan is written is `pikku-architect`; building against one, and the order plan-then-build, is `pikku-build`.
286
296
 
287
297
  ### A finished milestone is a tombstone
288
298
 
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: pikku-mantine
3
+ description: >-
4
+ Use when building a Mantine UI on top of a Pikku backend — rendering dates that came back from a
5
+ generated client, keeping layout flow-relative so the app survives an RTL locale, and branching on
6
+ colour scheme without hardcoding a shade. TRIGGER when: putting a value from usePikkuQuery or an
7
+ RPC response on screen, writing margins/padding/alignment in Mantine props or CSS, choosing a date
8
+ input, or handling light/dark. DO NOT TRIGGER when: the data does not come from a Pikku client
9
+ (this is only about what the generated clients hand you), or for user-facing copy (use pikku-i18n).
10
+ installGroups: [client]
11
+ ---
12
+
13
+ # Mantine on a Pikku client
14
+
15
+ ## Dates — always format before rendering
16
+
17
+ The generated clients run `transformDates`, which revives **fully-zoned ISO-8601 instants** —
18
+ `2026-03-14T08:12:00Z`, `2026-03-14T08:12:00.000+01:00` — into `Date` objects and touches nothing
19
+ else. A bare `2026-03-14`, a zoneless `2026-03-14T08:12:00` and an impossible `2026-02-31T00:00:00Z`
20
+ all stay the strings the server sent. So a field's runtime type follows the VALUE, not the schema:
21
+ one column can arrive as a `Date` from one row and a string from the next.
22
+
23
+ Two consequences, and both compile:
24
+
25
+ - **A string method on one white-screens the page.** `row.createdAt.split('T')[0]` type-checks
26
+ against nothing useful and blows up at runtime. There is no string to slice.
27
+ - **A raw `Date` dropped into JSX crashes the route.** `<Text>{row.createdAt}</Text>`, a table cell,
28
+ a `<Badge>` — React throws `Objects are not valid as a React child (found: [object Date])` and the
29
+ page falls into its error boundary. Nothing catches it before the screen is white, which makes it
30
+ the most common broken page in a build.
31
+
32
+ **Format with dayjs.** It is Mantine's own date library, already shipped alongside `@mantine/dates`,
33
+ and it takes either a `Date` or a string. Never `toLocaleDateString`, `date-fns` or `luxon`.
34
+
35
+ ```tsx
36
+ import dayjs from 'dayjs'
37
+
38
+ <Text>{dayjs(row.dueOn).format('D MMM YYYY')}</Text> // 15 Jun 2026
39
+ <Text>{dayjs(row.createdAt).format('D MMM YYYY, HH:mm')}</Text>
40
+ ```
41
+
42
+ A relative "2 days ago" via dayjs `relativeTime` is fine. Coercing instead of formatting
43
+ (`` `${d}` ``, `String(d)`, `d + ''`) does not crash but prints
44
+ `Mon Jun 15 2026 02:00:00 GMT+0200` — a different bug, equally wrong.
45
+
46
+ Date **inputs** are `@mantine/dates` — `DatePickerInput`, `DatePicker`, `Calendar` — never a raw
47
+ `<TextInput type="date">`. Those are pickers and they are not a schedule: Mantine ships no
48
+ week/time-grid component, so a diary, rota, timetable or booking week is a grid you build, not a
49
+ `Calendar` with the time-of-day left out. `Calendar` with `renderDay` IS right for "a few things on
50
+ each day of a month", like a content calendar or a holiday planner.
51
+
52
+ All of this applies to stub and fixture dates exactly as it does to real data.
53
+
54
+ ## RTL-safe styles
55
+
56
+ Write layout styles flow-relative so the UI works in both LTR and RTL languages. Pikku's i18n ships
57
+ Arabic, Hebrew, Farsi and Urdu support, and a physical margin is what breaks under it.
58
+
59
+ | Avoid | Use instead |
60
+ | ----------------------------- | ------------------------------------------ |
61
+ | `ml`, `mr`, `pl`, `pr` | `ms`, `me`, `ps`, `pe` |
62
+ | `text-align: left/right` | `text-align: start/end` |
63
+ | `margin-left`, `margin-right` | `margin-inline-start`, `margin-inline-end` |
64
+ | `flex-direction: row-reverse` | `dir` attribute or logical properties |
65
+
66
+ Mantine shorthand: `ms` = margin-inline-start, `me` = margin-inline-end, `ps` = padding-inline-start,
67
+ `pe` = padding-inline-end.
68
+
69
+ ## Dark mode
70
+
71
+ Use Mantine's `light-dark()` utility or `useMantineColorScheme`, and only with colours that already
72
+ come from the theme — never introduce a literal colour or a shade string for one scheme.
73
+
74
+ ```tsx
75
+ // Correct
76
+ <Box bg="var(--mantine-color-body)">
77
+
78
+ // Wrong — scheme branch with hardcoded Mantine shades
79
+ <Box bg={theme.colorScheme === 'dark' ? 'dark.6' : 'gray.0'}>
80
+ ```
@@ -147,6 +147,14 @@ it is optional because the same function can be reached over plain HTTP or RPC,
147
147
  where there is no stream to send on. The `if (channel)` guard is what lets one
148
148
  function serve both; the return value is the non-streaming answer.
149
149
 
150
+ A function that throws once the stream is open cannot answer with a status code,
151
+ so the runner ends the stream with `{ type: 'error', errorText }` then
152
+ `{ type: 'done' }`. A route whose client parses a different event protocol says
153
+ so with `streamProtocol`, and the failure is written in that one instead —
154
+ `streamProtocol: 'agui'` ends the stream with a single AG-UI `RUN_ERROR` and
155
+ nothing after it. The default is `'pikku'`; the generated agent stream routes
156
+ set `'agui'`.
157
+
150
158
  ### Generated Fetch Client
151
159
 
152
160
  After `npx pikku all`, a type-safe client is generated:
@@ -186,6 +186,63 @@ When you add a tool, tell whoever asked for it the URL. An assistant that cannot
186
186
  be pointed at an endpoint has not been connected to anything, and `/mcp` is the
187
187
  whole answer.
188
188
 
189
+ ## Authentication
190
+
191
+ An MCP endpoint is not gated as a whole. Pikku already knows, tool by tool, which
192
+ calls need a session, and the endpoint answers accordingly:
193
+
194
+ | Declaration | Anonymous call |
195
+ | ---------------------------------------- | ------------------- |
196
+ | `pikkuSessionlessFunc` with `mcp: true` | runs |
197
+ | the same, plus `auth: true` | `401` + a challenge |
198
+ | `pikkuFunc` with `mcp: true` | `401` + a challenge |
199
+
200
+ ```typescript
201
+ // public: anyone connecting to /mcp can call this
202
+ export const searchCatalog = pikkuSessionlessFunc<Query, Results>({
203
+ mcp: true,
204
+ func: async (services, data) => services.catalog.search(data),
205
+ })
206
+
207
+ // private: an anonymous caller is challenged, never dispatched
208
+ export const myOrders = pikkuFunc<void, Order[]>({
209
+ mcp: true,
210
+ func: async (services, _data, session) =>
211
+ services.orders.forUser(session.userId),
212
+ })
213
+ ```
214
+
215
+ The `401` carries a `WWW-Authenticate` header naming the endpoint's RFC 9728
216
+ Protected Resource Metadata document, which the server also serves — `/mcp` is
217
+ described at `/.well-known/oauth-protected-resource/mcp`. That pair is what an
218
+ MCP client needs to discover an authorization server and start an OAuth flow;
219
+ a refusal delivered as a JSON-RPC result instead reads to a client as a tool that
220
+ failed, and no discovery happens.
221
+
222
+ `tools/list` is never gated, so a client can still see what exists before it has
223
+ a token.
224
+
225
+ Nothing needs configuring: the metadata document defaults to advertising the
226
+ origin the request arrived on, which is right whenever the app is its own
227
+ authorization server. To point elsewhere, pass `mcpAuth` to the runtime:
228
+
229
+ ```typescript
230
+ new PikkuNodeHTTPServer(config, logger, {
231
+ mcpJson,
232
+ mcpAuth: {
233
+ authorizationServers: ['https://auth.example.com'],
234
+ scopesSupported: ['mcp'],
235
+ resourceName: 'Example API',
236
+ },
237
+ })
238
+ ```
239
+
240
+ The transport never verifies a token itself — session resolution stays with the
241
+ app's own middleware, exactly as it works over HTTP. One consequence: only a
242
+ request carrying *no* credentials is challenged. A token that is present but
243
+ expired is dispatched, and the runner's refusal reaches the client as a tool
244
+ error.
245
+
189
246
  ## Red flags
190
247
 
191
248
  | Symptom | Cause |
@@ -195,3 +252,5 @@ whole answer.
195
252
  | Resource returning `{ uri, blob, mimeType }` | Resources are text only: `{ uri, text }` |
196
253
  | Client sees a tool with no description | `mcp: true` without a `description` — check the codegen warning |
197
254
  | `/mcp` 404s | Nothing to serve yet — the mount is skipped until one tool, resource or prompt exists |
255
+ | A tool an assistant should be able to call returns `401` | It is a `pikkuFunc`, or declares `auth: true` — make it a `pikkuSessionlessFunc` if it is genuinely public |
256
+ | A private tool returns a result rather than a challenge | The request carried a credential, so it was dispatched; only a call with none is refused at the door |