@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/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +1 -6
- package/skills/pikku-architect/SKILL.md +35 -16
- package/skills/pikku-build/SKILL.md +29 -14
- package/skills/pikku-build/references/app.md +35 -24
- package/skills/pikku-fabric/SKILL.md +13 -0
- package/skills/pikku-knowledge/SKILL.md +12 -2
- package/skills/pikku-mantine/SKILL.md +80 -0
- package/skills/pikku-wiring/references/http.md +8 -0
- package/skills/pikku-wiring/references/mcp.md +59 -0
package/package.json
CHANGED
|
@@ -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`.
|
|
7
|
-
|
|
8
|
-
|
|
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.
|
|
21
|
-
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
264
|
-
plan with `plan show --for-build`,
|
|
265
|
-
`pikku knowledge plan progress` is clean. What you wrote is what
|
|
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
|
|
23
|
-
|
|
|
24
|
-
| A template was just cloned or scaffolded, and the tree still looks like one
|
|
25
|
-
| A real product, meant to be picked up by someone else
|
|
26
|
-
| A spike, a throwaway demo, an idea nobody has committed to
|
|
27
|
-
| A showcase meant to exercise every Pikku surface
|
|
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.**
|
|
57
|
-
|
|
58
|
-
`
|
|
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
|
|
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`
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
85
|
-
| --------------- |
|
|
86
|
-
| **Identifiers** | Function, component, type, variable and file names. Database tables and columns. Commit messages.
|
|
87
|
-
| **Meta** | `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role and persona descriptions
|
|
88
|
-
| **Product UI** | Every string the app shows a user
|
|
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: {
|
|
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:
|
|
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
|
|
373
|
-
fraction, plan only that fraction, and certify itself complete.
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
400
|
-
|
|
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
|
|
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
|
|
447
|
-
|
|
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:
|
|
468
|
-
metadata: { kind: 'json', tsType: 'PaymentMeta' },
|
|
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 —
|
|
554
|
-
rewriting
|
|
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:
|
|
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
|
|
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.
|
|
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 |
|