@pikku/skills 0.12.22 → 0.12.26

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.
Files changed (106) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +1 -1
  4. package/skills/pikku-a11y/SKILL.md +59 -0
  5. package/skills/pikku-addon/SKILL.md +2 -2
  6. package/skills/pikku-agent/SKILL.md +67 -316
  7. package/skills/pikku-agent/references/agents.md +299 -0
  8. package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
  9. package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
  10. package/skills/pikku-architect/SKILL.md +265 -0
  11. package/skills/pikku-auth/SKILL.md +89 -0
  12. package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +126 -34
  13. package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
  14. package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
  15. package/skills/pikku-auth/references/permissions.md +261 -0
  16. package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +0 -20
  17. package/skills/pikku-build/SKILL.md +88 -0
  18. package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +76 -24
  19. package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
  20. package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +56 -1
  21. package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
  22. package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
  23. package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +4 -20
  24. package/skills/{pikku-build-app → pikku-build}/references/ship.md +9 -3
  25. package/skills/pikku-concepts/SKILL.md +72 -7
  26. package/skills/pikku-concepts/references/concept-mapping.md +8 -8
  27. package/skills/pikku-deploy/SKILL.md +158 -0
  28. package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
  29. package/skills/pikku-deploy/references/cloudflare.md +104 -0
  30. package/skills/pikku-deploy/references/express.md +92 -0
  31. package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
  32. package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
  33. package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
  34. package/skills/pikku-deploy/references/uws.md +72 -0
  35. package/skills/pikku-deploy/references/ws.md +75 -0
  36. package/skills/pikku-emails/SKILL.md +3 -2
  37. package/skills/pikku-fabric/SKILL.md +47 -20
  38. package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
  39. package/skills/pikku-i18n/SKILL.md +62 -207
  40. package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
  41. package/skills/pikku-i18n/references/messages.md +218 -0
  42. package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
  43. package/skills/pikku-knowledge/SKILL.md +15 -0
  44. package/skills/pikku-kysely/SKILL.md +13 -13
  45. package/skills/pikku-list-query/SKILL.md +163 -0
  46. package/skills/pikku-meta/SKILL.md +58 -130
  47. package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
  48. package/skills/pikku-meta/references/meta.md +114 -0
  49. package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
  50. package/skills/pikku-middleware/SKILL.md +5 -5
  51. package/skills/pikku-n8n-import/SKILL.md +0 -1
  52. package/skills/pikku-permissions/SKILL.md +75 -229
  53. package/skills/pikku-react/SKILL.md +50 -298
  54. package/skills/pikku-react/references/client.md +313 -0
  55. package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
  56. package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
  57. package/skills/pikku-realtime/SKILL.md +110 -251
  58. package/skills/pikku-scenario/SKILL.md +60 -45
  59. package/skills/pikku-scenario/references/persona-run.md +148 -0
  60. package/skills/pikku-seo/SKILL.md +133 -0
  61. package/skills/pikku-service-backends/SKILL.md +154 -0
  62. package/skills/pikku-service-backends/references/aws.md +106 -0
  63. package/skills/pikku-service-backends/references/backblaze.md +57 -0
  64. package/skills/pikku-service-backends/references/mongodb.md +90 -0
  65. package/skills/pikku-service-backends/references/redis.md +75 -0
  66. package/skills/pikku-service-backends/references/schema.md +63 -0
  67. package/skills/pikku-services/SKILL.md +68 -291
  68. package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
  69. package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
  70. package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
  71. package/skills/pikku-services/references/services.md +272 -0
  72. package/skills/pikku-software-archaeology/README.md +5 -1
  73. package/skills/pikku-software-archaeology/SKILL.md +16 -2
  74. package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
  75. package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
  76. package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
  77. package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
  78. package/skills/pikku-webhook/SKILL.md +224 -0
  79. package/skills/pikku-wiring/SKILL.md +180 -0
  80. package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
  81. package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
  82. package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
  83. package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +3 -39
  84. package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
  85. package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
  86. package/skills/pikku-wiring/references/realtime.md +265 -0
  87. package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
  88. package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
  89. package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
  90. package/skills/pikku-workflow/SKILL.md +39 -2
  91. package/skills/pikku-aws/SKILL.md +0 -161
  92. package/skills/pikku-backblaze/SKILL.md +0 -104
  93. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
  94. package/skills/pikku-deploy-express/SKILL.md +0 -122
  95. package/skills/pikku-deploy-uws/SKILL.md +0 -144
  96. package/skills/pikku-mongodb/SKILL.md +0 -113
  97. package/skills/pikku-product-second-opinion/README.md +0 -43
  98. package/skills/pikku-redis/SKILL.md +0 -99
  99. package/skills/pikku-schema-ajv/SKILL.md +0 -83
  100. package/skills/pikku-schema-cfworker/SKILL.md +0 -82
  101. package/skills/pikku-ws/SKILL.md +0 -87
  102. /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
  103. /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
  104. /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
  105. /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
  106. /package/skills/{pikku-realtime/references/other-routes.md → pikku-wiring/references/realtime-other-routes.md} +0 -0
@@ -1,224 +1,79 @@
1
1
  ---
2
2
  name: pikku-i18n
3
- description: 'Wire i18n into a Pikku frontend with Paraglide JS (inlang). English by default, every user-facing string is a typed message function (`m.some__key()`) compiled from `messages/<locale>.json`, and additional languages are served under `/fr` `/de` URL prefixes. TRIGGER when: scaffolding or editing a frontend and writing user-facing text, adding a second language, or asked to "make this translatable / use tokens / add i18n". DO NOT TRIGGER for backend functions, error messages thrown from functions, or log output.'
4
- installGroups: [client, fabric]
3
+ description: >-
4
+ Use when writing user-facing text in a Pikku frontend, or making one speak another language.
5
+ Covers Paraglide JS message functions compiled from messages/<locale>.json, adding a second
6
+ language, generated enum-label maps with @pikku/paraglide, and right-to-left support for Arabic,
7
+ Hebrew, Farsi and Urdu. TRIGGER when: scaffolding or editing a frontend and writing display
8
+ text, asked to make copy translatable, adding a language, labelling an enum/status/role value,
9
+ or asked to support RTL / mirror the layout. DO NOT TRIGGER for backend functions, error
10
+ messages thrown from functions, or log output — none of those are display strings.
11
+ installGroups: [client]
5
12
  ---
6
13
 
7
- # Pikku i18n (Paraglide JS)
14
+ # Pikku i18n
8
15
 
9
- ## Agent Operating Procedure
16
+ ## Every visible string is a message
10
17
 
11
- Use this skill as an execution checklist, not reference material.
18
+ Never hardcode display text: add a key to `messages/en.json` and render
19
+ `m.the__key()`. This holds even in an app that will only ever ship English —
20
+ the messages are the seam a second language slots into, and the deploy pipeline
21
+ type-checks them, so an i18n mistake blocks the build rather than the release.
12
22
 
13
- 1. Every user-facing string in a frontend is a message. Never hardcode display text — add a key to `messages/en.json` and render `m.the__key()`. This holds even when the app ships only English; the messages are the seam a second language slots into later.
14
- 2. One `messages/<locale>.json` per language at the app root (NOT under `src/`), declared in `project.inlang/settings.json`. English (`en`) is `baseLocale` and the only locale until someone adds another. **`baseLocale` stays `en` whatever language the product speaks** — see [The product's language is not the code's language](#the-products-language-is-not-the-codes-language), which is the first thing to read if the brief says the app is not in English.
15
- 3. Messages compile to typed ESM functions in `src/paraglide/` (generated, self-gitignored — never edit or commit it). The Vite plugin compiles during `dev`/`build` with HMR on message edits; run the CLI compile only when you need `tsc` before Vite has ever run.
16
- 4. Validate with the app's own `tsc` then its `build`. The deploy pipeline compiles Paraglide and runs each frontend's `tsc` before building it — an i18n mistake blocks the deploy.
23
+ ## Pick the reference
17
24
 
18
- ## The product's language is not the code's language
25
+ | You are… | Read |
26
+ | --- | --- |
27
+ | Writing copy, wiring Paraglide, or adding a language | `references/messages.md` |
28
+ | Labelling an enum, status, kind or role value | `references/enum-labels.md` |
29
+ | Adding Arabic (or Hebrew, Farsi, Urdu), or writing layout styles | `references/rtl.md` |
19
30
 
20
- A brief that says "the entire UI is German, no English strings visible anywhere"
21
- is a statement about **one** of three separate things, and reading it as a
22
- statement about the codebase is the single most expensive mistake available in
23
- this skill. Three axes:
31
+ ## Three axes, and a brief usually means only one
24
32
 
25
- | Axis | What it covers | What sets it |
26
- | --------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
27
- | **Identifiers** | Function, component, type and file names; database tables and columns | Nothing — always English, no setting |
28
- | **Meta** | `description` / `name` / `title` / `template` authored inside the code, which the Pikku Console renders | `metaLocale` in `pikku.config.json`, default `en` |
29
- | **Product UI** | Every string the app shows a user | `messages/<locale>.json` + `defaultLocale` — **this axis only** |
33
+ "The entire UI is German" is a statement about the product, not about the
34
+ codebase. Reading it as one about the codebase is the most expensive mistake
35
+ available here.
30
36
 
31
- A non-English product moves the third row and nothing else.
37
+ | Axis | What it covers | What sets it |
38
+ | --- | --- | --- |
39
+ | **Identifiers** | Function, component, type and file names; tables and columns | Nothing — always English |
40
+ | **Meta** | `description` / `name` / `title` authored in code, rendered by the console | `metaLocale` in `pikku.config.json` |
41
+ | **Product UI** | Every string the app shows a user | `messages/<locale>.json` + `defaultLocale` |
32
42
 
33
- ### `baseLocale` stays `en`
43
+ `baseLocale` stays `en` whatever language the product speaks — it names the
44
+ message *source* catalogue every other locale is derived from, not the language
45
+ the app is in. Set `defaultLocale` instead.
34
46
 
35
- `baseLocale` in `project.inlang/settings.json` does not mean "the language the
36
- app is in". It names the message **source** — the catalogue every other locale is
37
- cloned from and translated against. Setting it to the product's language looks
38
- like it works, because the app does come up in that language, and then:
47
+ ## Direction is one setting, not per-component work
39
48
 
40
- - there is no `en.json`, so `--add-locale` has no catalogue to translate from
41
- - the app can never gain a second language without re-authoring every key
42
- - a message missing from a locale falls back to a catalogue nobody wrote
43
-
44
- The setting that actually decides what a first-time visitor sees is
45
- `defaultLocale`, held in `apps/app/src/i18n/active.json` in the Fabric app
46
- template and read by `src/i18n/config.ts`. It is deliberately a separate file
47
- from `settings.json` for exactly this reason — the source language and the
48
- served language are different questions.
49
-
50
- So a German medical portal is **three** settings, not one:
51
-
52
- ```jsonc
53
- // project.inlang/settings.json — the source catalogue is English
54
- { "baseLocale": "en", "locales": ["en", "de"] }
55
-
56
- // apps/app/src/i18n/active.json — what a visitor opens in
57
- { "defaultLocale": "de" }
58
-
59
- // pikku.config.json — the language the team reads their Console in
60
- { "metaLocale": "de" }
61
- ```
62
-
63
- In the Fabric template both of the first two have a command, so you rarely edit
64
- them by hand:
65
-
66
- ```sh
67
- fabric i18n --add-locale de # adds "de" to locales, seeds messages/de.json from en.json
68
- fabric i18n --default-locale de # writes active.json — the app now OPENS in German
69
- ```
70
-
71
- ### The failure this is written from
72
-
73
- A real build, from this template. The brief said the UI was German; the agent
74
- set `baseLocale: "de"` with `locales: ["de"]` and no `en.json`, then carried the
75
- same reading into the code — RPC functions `getUebersicht` and
76
- `getPatientendetail`, components `Zeitstrahl` and `AufmerksamkeitStreifen`,
77
- helpers `datumDeutsch` and `voraussichtlichFertig`, database tables `vorgang`
78
- and `ereignis` with German columns.
79
-
80
- The German UI it was asked for needed none of that. It needed German **values**
81
- in a catalogue whose keys and source stayed English. What it got instead was a
82
- project that cannot add a second language and cannot be picked up by anyone who
83
- does not read German.
84
-
85
- If you find a project in this state, say so plainly rather than working around
86
- it: `baseLocale` cannot be repointed without re-keying every message, so it is a
87
- migration someone has to agree to, not a fix to slip in.
88
-
89
- ## The moving parts (starter-template layout)
90
-
91
- - `messages/en.json` — flat keys, `{param}` interpolation, inlang message-format:
92
- ```json
93
- {
94
- "$schema": "https://inlang.com/schema/inlang-message-format",
95
- "auth__login__title": "Sign in",
96
- "auth__login__description": "Welcome back to {name}."
97
- }
98
- ```
99
- Key convention: lower snake_case, `__` (double underscore) between namespace segments, `_` within a segment — `auth__login__title`, `common__email_placeholder`.
100
- - `project.inlang/settings.json` — `baseLocale`, `locales`, the `@inlang/plugin-message-format` module, `pathPattern: "./messages/{locale}.json"`.
101
- - `vite.config.ts` — `paraglideVitePlugin({ project: './project.inlang', outdir: './src/paraglide' })` from `@inlang/paraglide-js` (devDependency), FIRST in the plugins array.
102
- - `src/paraglide/` — compiled output (`messages.js`, `runtime.js`, per-locale `messages/*.js`). Generated; it writes its own `.gitignore`.
103
- - `src/i18n/config.ts` — locale plumbing, and the ONLY hand-written i18n module: `supportedLocales`/`defaultLocale` (re-exported from `../paraglide/runtime.js`), `detectLocale`, `localeDir` (RTL for ar/he/fa/ur), a reactive locale store (`overwriteGetLocale` bridged to `useSyncExternalStore`), `setActiveLocale`, `useLocale()`. This is not a wrapper over messages — Paraglide's `getLocale()` is a module global with no React reactivity, and this bridges it. Wire `overwriteGetLocale` or `m.*()` will resolve a different locale than the app thinks is active.
104
- - `tsconfig.json` — `"allowJs": true, "checkJs": false` so `tsc` can consume Paraglide's JSDoc-typed JS output.
105
-
106
- ## Using messages in components
107
-
108
- ```tsx
109
- import { m } from '../paraglide/messages.js'
110
- import { useLocale } from '@/i18n/config'
111
-
112
- function LoginPage() {
113
- useLocale() // subscribe: re-render m.*() when the locale switches
114
- return (
115
- <>
116
- <Title>{m.auth__login__title()}</Title>
117
- <Text>{m.auth__login__description({ name: m.app__name() })}</Text>
118
- </>
119
- )
120
- }
121
- ```
122
-
123
- - Params: `{name}` in the JSON → `m.auth__login__description({ name })`. Params are typed per message.
124
- - Any component that renders `m.*()` calls `useLocale()` (bare call is enough); it also returns `{ locale, dir, setLocale }` for switchers.
125
- - Non-component helpers (formatters, status maps) call `m.some__key()` directly — the functions are plain ESM, no hook needed; the render-time subscription lives in the component that displays the result.
126
- - Locale switching: the root route persists to localStorage, sets `<html lang dir>` (`localeDir`), and calls `setActiveLocale` — in-SPA re-render, no page reload. Mirror `routes/__root.tsx` in the starter template.
127
-
128
- ## Keys only known at runtime (enum labels, status maps)
129
-
130
- A DB value picking a label is the one case a generated message can't express.
131
- Paraglide's README (§ "What about dynamic or CMS-driven keys?") is explicit: use
132
- an **explicit mapping from value to message function**. Key it on the enum type,
133
- never `string`:
134
-
135
- ```ts
136
- import { m } from '../paraglide/messages.js'
137
-
138
- const DOCUMENT_STATUS_LABEL: Record<DocumentStatus, () => string> = {
139
- completed: m.enum__document_status__completed,
140
- in_progress: m.enum__document_status__in_progress,
141
- required: m.enum__document_status__required,
142
- }
143
-
144
- // call site — no fallback, because there is no missing case
145
- DOCUMENT_STATUS_LABEL[status]()
146
- ```
147
-
148
- `Record<DocumentStatus, …>` is exhaustive: add a value to the enum without a
149
- label and the build fails. That is the entire point.
150
-
151
- **Don't write these maps by hand.** `@pikku/paraglide` generates them from the
152
- `enum__<group>__<member>` keys in the catalog and types each one against the DB
153
- enum it mirrors, so a migration adding a status is a compile error rather than a
154
- map someone forgot. Use the namespace above (singular `enum`, `__` between
155
- segments) so the generator picks the group up, and read `pikku-paraglide` before
156
- adding one.
157
-
158
- Do NOT write `Record<string, () => string>` with a `?? status` fallback, and do
159
- NOT index the namespace with a computed key (`m[\`enums__${name}__${value}\`]`).
160
- Both compile, both render the raw identifier to users when a label is missing,
161
- and both reintroduce exactly the silent-fallback failure Paraglide exists to
162
- eliminate. If you find yourself writing a `resolveDynamicKey(key: string)`
163
- helper, stop — that helper IS the bug.
164
-
165
- ## Type safety — and why deploys block on i18n
166
-
167
- A message IS a function: a typo'd or deleted key (`m.auth__login__titel()`) is a missing export — a **TypeScript error**, not a silent runtime fallback string. Params are typed too. The deploy pipeline compiles Paraglide then runs each frontend's `tsc` (`"tsc": "tsc --noEmit"` script — keep it in every frontend's `package.json`) **before** building; a type error aborts the deploy. `vite build` does not type-check on its own, so this gate is the only thing standing between a broken message and production.
168
-
169
- The gate catches _invalid_ messages but not _inlined_ strings. The `@pikku/mantine` `I18nNode` prop typing catches those: a raw string literal fails to compile on a gated prop, because `I18nString` is a branded type a bare `string` can't satisfy. Between the two, `tsc` is the whole safety net — there is no runtime fallback to inspect, by design.
170
-
171
- ## Compile step
172
-
173
- - **Dev/build:** the Vite plugin compiles automatically; editing `messages/*.json` under a running dev server recompiles + HMRs.
174
- - **Standalone `tsc` before Vite has run** (fresh clone, CI):
175
- ```sh
176
- npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/paraglide
177
- ```
178
- This is exactly what the deploy CI does before the per-app `tsc`.
179
-
180
- ## Adding a second language
181
-
182
- 1. `messages/fr.json` mirroring `en.json`'s keys (translate the values, keep `{param}` names identical).
183
- 2. Add `"fr"` to `locales` in `project.inlang/settings.json`. **Leave `baseLocale` at `en`** — step 1 only works because there is an English catalogue to mirror.
184
- 3. Recompile (restart/`vite dev` or the CLI compile). A locale file missing keys falls back to the base locale per message.
185
- 4. Content is reachable via the `/<lang>` URL prefix (`detectLocale` already resolves it); the base locale needs no prefix. Expose the switcher via `useLocale().setLocale`.
186
- 5. Only if the app should **open** in the new language rather than merely offer it: set `defaultLocale` (`active.json` / `fabric i18n --default-locale fr`). Adding a locale and changing the default are different asks — do the second only when asked.
187
-
188
- ## i18n debug mode (find inlined strings)
189
-
190
- `tsc` catches invalid messages, and the `@pikku/mantine` gate catches raw strings on gated props — but neither sees a hardcoded string in plain JSX, an `aria-label`, `alt`, `document.title`, or anything passed to a non-Mantine component. Debug mode covers that gap: render every message as block glyphs (`█`), and whatever is still readable never went through a message.
191
-
192
- **Build it as a generated locale, never as a runtime wrapper.** Masked text is text, and rendering different text per locale is what Paraglide already does:
193
-
194
- 1. A script generates `messages/zz.json` from `en.json`, replacing `\S` with `█` while leaving `{placeholders}` intact (they are message inputs — mangling them changes the compiled signature). Run it before `paraglide-js compile`; gitignore the output.
195
- 2. Add `"zz"` to `locales` in `project.inlang/settings.json`.
196
- 3. Switch to it in the locale bridge:
197
- ```ts
198
- overwriteGetLocale(() => (isI18nDebug() ? 'zz' : activeLocale))
199
- ```
200
-
201
- Keep `zz` out of the app's own `supportedLocales` — that drives URL prefixes, hreflang and any backend `locale` param, none of which should see it.
202
-
203
- Generate the catalogue in dev only. With `messages/zz.json` absent, Paraglide compiles `zz` to an alias of the base locale (`const zz_x = en_x` — one line per message, no duplicated strings), so a production bundle carries the locale at effectively zero cost.
204
-
205
- Both the generator and the store bridge are being upstreamed (pikkujs/pikku#1036, #1035).
206
-
207
- The wrapper alternative — a module that walks the namespace and pipes each message through a `mask()` — is what this replaces. It defeats tree-shaking (touching every export), adds a check on every call, and forces every component to import `m` from the wrapper instead of Paraglide.
49
+ Set `dir` once at the document root from the active locale and the browser (and
50
+ Mantine) mirror everything — provided every custom style is flow-relative
51
+ (`margin-inline-start`, `text-align: start`, Mantine `ms`/`me`) rather than
52
+ physical (`margin-left`, `text-align: left`, `ml`/`me`'s physical twins). Write
53
+ logical properties from the start even in an English-only app; that discipline
54
+ is what makes an RTL language just another locale file.
208
55
 
209
56
  ## What NOT to do
210
57
 
211
- - Don't hardcode display strings "just for now" — the message is the work.
212
- - Don't set `baseLocale` to anything but `en`, whatever language the product speaks. It names the source catalogue, and a project without one can never add a language. Set `defaultLocale` instead.
213
- - Don't let a non-English UI reach the identifiers. Functions, components, types, files, tables and columns are English in every project; the product's language lives in `messages/*.json` and nowhere else.
214
- - Don't translate message **keys**. `auth__login__title` stays English in `de.json`; only the value changes.
215
- - Don't edit or commit anything under `src/paraglide/` — it's regenerated; change `messages/*.json` instead.
216
- - **Don't wrap `m`.** No re-export module, no branding layer, no resolver. Components import `m` from `../paraglide/messages.js` and call it. `@pikku/react`'s `I18nString` is declared as `string & { readonly __brand: 'LocalizedString' }` — deliberately identical to Paraglide's own `LocalizedString` — so `m.some__key()` satisfies the `@pikku/mantine` `I18nNode` gate natively. A wrapper adds nothing and costs per-message tree-shaking.
217
-
218
- `packages/console` is the one place in this repo that still wraps it, in `src/i18n/messages.ts`, to keep the debug mask (`█`) it carried over from i18next. That wrapper is a leftover, not a pattern — the generated-locale approach above is how a new app gets the same masking without touching every export. Don't copy it.
219
-
220
- The `mKey`/`mList` runtime resolvers that used to live beside it are **gone**, and must not come back. `mList` resolved indexed `prefix.0` keys that no longer exist; `mKey` took a computed string, which is exactly the type safety Paraglide exists to provide. Where a key really is dynamic, map the discriminant to a message _function_ and call it — the map is type-checked, a string is not.
221
-
222
- - Don't re-resolve messages by string key or re-implement `{param}` interpolation. A key-string resolver turns a missing key back into silent runtime text, surrendering the type safety that is the entire reason to use Paraglide.
223
- - Don't reach for i18next/react-i18next or a runtime-fetch translation loader — Paraglide's compiled functions are the whole delivery mechanism.
224
- - Don't tokenize backend error messages or logs here — those are not frontend display strings.
58
+ - **Do not resolve a message key at runtime.** No `mKey('status.' + value)`, no
59
+ `m['enum__' + x]()`, no key-string resolver. A computed key cannot be
60
+ type-checked or tree-shaken, so a renamed message degrades to silent runtime
61
+ text. Where the key is genuinely dynamic, map the discriminant to a message
62
+ *function* — the map is checked, a string is not.
63
+ - **Do not `asI18n()` a hardcoded English string.** `asI18n` exists to pass
64
+ opaque server data (a name, a slug, an id) through the i18n gate. An enum value
65
+ goes through its generated label map.
66
+ - **Do not wrap `m` without a reason you can name.** `m.some__key()` already
67
+ satisfies the `I18nNode` gate, so a plain re-export module adds nothing and
68
+ costs per-message tree-shaking. Wrapping the namespace is only worth it when it
69
+ buys a feature the gate cannot — debug masking of translated copy, say — and
70
+ then the catalogue has to be small enough to ship whole.
71
+ - **Do not translate message keys.** `auth__login__title` stays English in
72
+ `de.json`; only the value changes.
73
+ - **Do not edit or commit `src/paraglide/`, `i18n-enum.gen.ts` or `enums.gen.ts`.**
74
+ Change the catalogue or the migration and regenerate.
75
+ - **Do not fake RTL** with `flex-direction: row-reverse`, reversed DOM order, or
76
+ per-locale layout branches. They double-flip the moment direction changes. DOM
77
+ order is logical order; let `dir` decide the visual one.
78
+ - **Do not reach for i18next or a runtime translation loader.** Paraglide's
79
+ compiled functions are the whole delivery mechanism.
@@ -1,9 +1,3 @@
1
- ---
2
- name: pikku-paraglide
3
- description: 'Generate typed, static enum-label maps for a Paraglide i18n frontend with `@pikku/paraglide`, and reconcile them against the database enum columns so a label can never silently drift from a DB value. Enum-valued labels live under a reserved `enum__<group>__<member>` message namespace; the generator emits `i18n-enum.gen.ts` typed `satisfies EnumLabel<DbEnum>`. TRIGGER when: labelling an enum/status/kind/role value in a Paraglide app, replacing a dynamic `mKey(...)`/`m[...]` lookup with a static map, wiring `@pikku/paraglide` into Vite, or reconciling i18n against `CHECK (col IN (...))` / Postgres enum columns. DO NOT TRIGGER for plain free-text UI copy (that is a normal `m.some_key()` message), backend errors, or logs.'
4
- installGroups: [client]
5
- ---
6
-
7
1
  # Pikku Paraglide enum labels
8
2
 
9
3
  ## Agent Operating Procedure
@@ -0,0 +1,218 @@
1
+ # Pikku i18n (Paraglide JS)
2
+
3
+ ## Agent Operating Procedure
4
+
5
+ Use this skill as an execution checklist, not reference material.
6
+
7
+ 1. Every user-facing string in a frontend is a message. Never hardcode display text — add a key to `messages/en.json` and render `m.the__key()`. This holds even when the app ships only English; the messages are the seam a second language slots into later.
8
+ 2. One `messages/<locale>.json` per language at the app root (NOT under `src/`), declared in `project.inlang/settings.json`. English (`en`) is `baseLocale` and the only locale until someone adds another. **`baseLocale` stays `en` whatever language the product speaks** — see [The product's language is not the code's language](#the-products-language-is-not-the-codes-language), which is the first thing to read if the brief says the app is not in English.
9
+ 3. Messages compile to typed ESM functions in `src/paraglide/` (generated, self-gitignored — never edit or commit it). The Vite plugin compiles during `dev`/`build` with HMR on message edits; run the CLI compile only when you need `tsc` before Vite has ever run.
10
+ 4. Validate with the app's own `tsc` then its `build`. The deploy pipeline compiles Paraglide and runs each frontend's `tsc` before building it — an i18n mistake blocks the deploy.
11
+
12
+ ## The product's language is not the code's language
13
+
14
+ A brief that says "the entire UI is German, no English strings visible anywhere"
15
+ is a statement about **one** of three separate things, and reading it as a
16
+ statement about the codebase is the single most expensive mistake available in
17
+ this skill. Three axes:
18
+
19
+ | Axis | What it covers | What sets it |
20
+ | --------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
21
+ | **Identifiers** | Function, component, type and file names; database tables and columns | Nothing — always English, no setting |
22
+ | **Meta** | `description` / `name` / `title` / `template` authored inside the code, which the Pikku Console renders | `metaLocale` in `pikku.config.json`, default `en` |
23
+ | **Product UI** | Every string the app shows a user | `messages/<locale>.json` + `defaultLocale` — **this axis only** |
24
+
25
+ A non-English product moves the third row and nothing else.
26
+
27
+ ### `baseLocale` stays `en`
28
+
29
+ `baseLocale` in `project.inlang/settings.json` does not mean "the language the
30
+ app is in". It names the message **source** — the catalogue every other locale is
31
+ cloned from and translated against. Setting it to the product's language looks
32
+ like it works, because the app does come up in that language, and then:
33
+
34
+ - there is no `en.json`, so `--add-locale` has no catalogue to translate from
35
+ - the app can never gain a second language without re-authoring every key
36
+ - a message missing from a locale falls back to a catalogue nobody wrote
37
+
38
+ The setting that actually decides what a first-time visitor sees is
39
+ `defaultLocale`, held in `apps/app/src/i18n/active.json` in the Fabric app
40
+ template and read by `src/i18n/config.ts`. It is deliberately a separate file
41
+ from `settings.json` for exactly this reason — the source language and the
42
+ served language are different questions.
43
+
44
+ So a German medical portal is **three** settings, not one:
45
+
46
+ ```jsonc
47
+ // project.inlang/settings.json — the source catalogue is English
48
+ { "baseLocale": "en", "locales": ["en", "de"] }
49
+
50
+ // apps/app/src/i18n/active.json — what a visitor opens in
51
+ { "defaultLocale": "de" }
52
+
53
+ // pikku.config.json — the language the team reads their Console in
54
+ { "metaLocale": "de" }
55
+ ```
56
+
57
+ In the Fabric template both of the first two have a command, so you rarely edit
58
+ them by hand:
59
+
60
+ ```sh
61
+ fabric i18n --add-locale de # adds "de" to locales, seeds messages/de.json from en.json
62
+ fabric i18n --default-locale de # writes active.json — the app now OPENS in German
63
+ ```
64
+
65
+ ### The failure this is written from
66
+
67
+ A real build, from this template. The brief said the UI was German; the agent
68
+ set `baseLocale: "de"` with `locales: ["de"]` and no `en.json`, then carried the
69
+ same reading into the code — RPC functions `getUebersicht` and
70
+ `getPatientendetail`, components `Zeitstrahl` and `AufmerksamkeitStreifen`,
71
+ helpers `datumDeutsch` and `voraussichtlichFertig`, database tables `vorgang`
72
+ and `ereignis` with German columns.
73
+
74
+ The German UI it was asked for needed none of that. It needed German **values**
75
+ in a catalogue whose keys and source stayed English. What it got instead was a
76
+ project that cannot add a second language and cannot be picked up by anyone who
77
+ does not read German.
78
+
79
+ If you find a project in this state, say so plainly rather than working around
80
+ it: `baseLocale` cannot be repointed without re-keying every message, so it is a
81
+ migration someone has to agree to, not a fix to slip in.
82
+
83
+ ## The moving parts (starter-template layout)
84
+
85
+ - `messages/en.json` — flat keys, `{param}` interpolation, inlang message-format:
86
+ ```json
87
+ {
88
+ "$schema": "https://inlang.com/schema/inlang-message-format",
89
+ "auth__login__title": "Sign in",
90
+ "auth__login__description": "Welcome back to {name}."
91
+ }
92
+ ```
93
+ Key convention: lower snake_case, `__` (double underscore) between namespace segments, `_` within a segment — `auth__login__title`, `common__email_placeholder`.
94
+ - `project.inlang/settings.json` — `baseLocale`, `locales`, the `@inlang/plugin-message-format` module, `pathPattern: "./messages/{locale}.json"`.
95
+ - `vite.config.ts` — `paraglideVitePlugin({ project: './project.inlang', outdir: './src/paraglide' })` from `@inlang/paraglide-js` (devDependency), FIRST in the plugins array.
96
+ - `src/paraglide/` — compiled output (`messages.js`, `runtime.js`, per-locale `messages/*.js`). Generated; it writes its own `.gitignore`.
97
+ - `src/i18n/config.ts` — locale plumbing, and the ONLY hand-written i18n module: `supportedLocales`/`defaultLocale` (re-exported from `../paraglide/runtime.js`), `detectLocale`, `localeDir` (RTL for ar/he/fa/ur), a reactive locale store (`overwriteGetLocale` bridged to `useSyncExternalStore`), `setActiveLocale`, `useLocale()`. This is not a wrapper over messages — Paraglide's `getLocale()` is a module global with no React reactivity, and this bridges it. Wire `overwriteGetLocale` or `m.*()` will resolve a different locale than the app thinks is active.
98
+ - `tsconfig.json` — `"allowJs": true, "checkJs": false` so `tsc` can consume Paraglide's JSDoc-typed JS output.
99
+
100
+ ## Using messages in components
101
+
102
+ ```tsx
103
+ import { m } from '../paraglide/messages.js'
104
+ import { useLocale } from '@/i18n/config'
105
+
106
+ function LoginPage() {
107
+ useLocale() // subscribe: re-render m.*() when the locale switches
108
+ return (
109
+ <>
110
+ <Title>{m.auth__login__title()}</Title>
111
+ <Text>{m.auth__login__description({ name: m.app__name() })}</Text>
112
+ </>
113
+ )
114
+ }
115
+ ```
116
+
117
+ - Params: `{name}` in the JSON → `m.auth__login__description({ name })`. Params are typed per message.
118
+ - Any component that renders `m.*()` calls `useLocale()` (bare call is enough); it also returns `{ locale, dir, setLocale }` for switchers.
119
+ - Non-component helpers (formatters, status maps) call `m.some__key()` directly — the functions are plain ESM, no hook needed; the render-time subscription lives in the component that displays the result.
120
+ - Locale switching: the root route persists to localStorage, sets `<html lang dir>` (`localeDir`), and calls `setActiveLocale` — in-SPA re-render, no page reload. Mirror `routes/__root.tsx` in the starter template.
121
+
122
+ ## Keys only known at runtime (enum labels, status maps)
123
+
124
+ A DB value picking a label is the one case a generated message can't express.
125
+ Paraglide's README (§ "What about dynamic or CMS-driven keys?") is explicit: use
126
+ an **explicit mapping from value to message function**. Key it on the enum type,
127
+ never `string`:
128
+
129
+ ```ts
130
+ import { m } from '../paraglide/messages.js'
131
+
132
+ const DOCUMENT_STATUS_LABEL: Record<DocumentStatus, () => string> = {
133
+ completed: m.enum__document_status__completed,
134
+ in_progress: m.enum__document_status__in_progress,
135
+ required: m.enum__document_status__required,
136
+ }
137
+
138
+ // call site — no fallback, because there is no missing case
139
+ DOCUMENT_STATUS_LABEL[status]()
140
+ ```
141
+
142
+ `Record<DocumentStatus, …>` is exhaustive: add a value to the enum without a
143
+ label and the build fails. That is the entire point.
144
+
145
+ **Don't write these maps by hand.** `@pikku/paraglide` generates them from the
146
+ `enum__<group>__<member>` keys in the catalog and types each one against the DB
147
+ enum it mirrors, so a migration adding a status is a compile error rather than a
148
+ map someone forgot. Use the namespace above (singular `enum`, `__` between
149
+ segments) so the generator picks the group up, and read `references/enum-labels.md` before
150
+ adding one.
151
+
152
+ Do NOT write `Record<string, () => string>` with a `?? status` fallback, and do
153
+ NOT index the namespace with a computed key (`m[\`enums__${name}__${value}\`]`).
154
+ Both compile, both render the raw identifier to users when a label is missing,
155
+ and both reintroduce exactly the silent-fallback failure Paraglide exists to
156
+ eliminate. If you find yourself writing a `resolveDynamicKey(key: string)`
157
+ helper, stop — that helper IS the bug.
158
+
159
+ ## Type safety — and why deploys block on i18n
160
+
161
+ A message IS a function: a typo'd or deleted key (`m.auth__login__titel()`) is a missing export — a **TypeScript error**, not a silent runtime fallback string. Params are typed too. The deploy pipeline compiles Paraglide then runs each frontend's `tsc` (`"tsc": "tsc --noEmit"` script — keep it in every frontend's `package.json`) **before** building; a type error aborts the deploy. `vite build` does not type-check on its own, so this gate is the only thing standing between a broken message and production.
162
+
163
+ The gate catches _invalid_ messages but not _inlined_ strings. The `@pikku/mantine` `I18nNode` prop typing catches those: a raw string literal fails to compile on a gated prop, because `I18nString` is a branded type a bare `string` can't satisfy. Between the two, `tsc` is the whole safety net — there is no runtime fallback to inspect, by design.
164
+
165
+ ## Compile step
166
+
167
+ - **Dev/build:** the Vite plugin compiles automatically; editing `messages/*.json` under a running dev server recompiles + HMRs.
168
+ - **Standalone `tsc` before Vite has run** (fresh clone, CI):
169
+ ```sh
170
+ npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/paraglide
171
+ ```
172
+ This is exactly what the deploy CI does before the per-app `tsc`.
173
+
174
+ ## Adding a second language
175
+
176
+ 1. `messages/fr.json` mirroring `en.json`'s keys (translate the values, keep `{param}` names identical).
177
+ 2. Add `"fr"` to `locales` in `project.inlang/settings.json`. **Leave `baseLocale` at `en`** — step 1 only works because there is an English catalogue to mirror.
178
+ 3. Recompile (restart/`vite dev` or the CLI compile). A locale file missing keys falls back to the base locale per message.
179
+ 4. Content is reachable via the `/<lang>` URL prefix (`detectLocale` already resolves it); the base locale needs no prefix. Expose the switcher via `useLocale().setLocale`.
180
+ 5. Only if the app should **open** in the new language rather than merely offer it: set `defaultLocale` (`active.json` / `fabric i18n --default-locale fr`). Adding a locale and changing the default are different asks — do the second only when asked.
181
+
182
+ ## i18n debug mode (find inlined strings)
183
+
184
+ `tsc` catches invalid messages, and the `@pikku/mantine` gate catches raw strings on gated props — but neither sees a hardcoded string in plain JSX, an `aria-label`, `alt`, `document.title`, or anything passed to a non-Mantine component. Debug mode covers that gap: render every message as block glyphs (`█`), and whatever is still readable never went through a message.
185
+
186
+ **Build it as a generated locale, never as a runtime wrapper.** Masked text is text, and rendering different text per locale is what Paraglide already does:
187
+
188
+ 1. A script generates `messages/zz.json` from `en.json`, replacing `\S` with `█` while leaving `{placeholders}` intact (they are message inputs — mangling them changes the compiled signature). Run it before `paraglide-js compile`; gitignore the output.
189
+ 2. Add `"zz"` to `locales` in `project.inlang/settings.json`.
190
+ 3. Switch to it in the locale bridge:
191
+ ```ts
192
+ overwriteGetLocale(() => (isI18nDebug() ? 'zz' : activeLocale))
193
+ ```
194
+
195
+ Keep `zz` out of the app's own `supportedLocales` — that drives URL prefixes, hreflang and any backend `locale` param, none of which should see it.
196
+
197
+ Generate the catalogue in dev only. With `messages/zz.json` absent, Paraglide compiles `zz` to an alias of the base locale (`const zz_x = en_x` — one line per message, no duplicated strings), so a production bundle carries the locale at effectively zero cost.
198
+
199
+ Both the generator and the store bridge are being upstreamed (pikkujs/pikku#1036, #1035).
200
+
201
+ The wrapper alternative — a module that walks the namespace and pipes each message through a `mask()` — is what this replaces. It defeats tree-shaking (touching every export), adds a check on every call, and forces every component to import `m` from the wrapper instead of Paraglide.
202
+
203
+ ## What NOT to do
204
+
205
+ - Don't hardcode display strings "just for now" — the message is the work.
206
+ - Don't set `baseLocale` to anything but `en`, whatever language the product speaks. It names the source catalogue, and a project without one can never add a language. Set `defaultLocale` instead.
207
+ - Don't let a non-English UI reach the identifiers. Functions, components, types, files, tables and columns are English in every project; the product's language lives in `messages/*.json` and nowhere else.
208
+ - Don't translate message **keys**. `auth__login__title` stays English in `de.json`; only the value changes.
209
+ - Don't edit or commit anything under `src/paraglide/` — it's regenerated; change `messages/*.json` instead.
210
+ - **Don't wrap `m`.** No re-export module, no branding layer, no resolver. Components import `m` from `../paraglide/messages.js` and call it. `@pikku/react`'s `I18nString` is declared as `string & { readonly __brand: 'LocalizedString' }` — deliberately identical to Paraglide's own `LocalizedString` — so `m.some__key()` satisfies the `@pikku/mantine` `I18nNode` gate natively. A wrapper adds nothing and costs per-message tree-shaking.
211
+
212
+ `packages/console` is the one place in this repo that still wraps it, in `src/i18n/messages.ts`, to keep the debug mask (`█`) it carried over from i18next. That wrapper is a leftover, not a pattern — the generated-locale approach above is how a new app gets the same masking without touching every export. Don't copy it.
213
+
214
+ The `mKey`/`mList` runtime resolvers that used to live beside it are **gone**, and must not come back. `mList` resolved indexed `prefix.0` keys that no longer exist; `mKey` took a computed string, which is exactly the type safety Paraglide exists to provide. Where a key really is dynamic, map the discriminant to a message _function_ and call it — the map is type-checked, a string is not.
215
+
216
+ - Don't re-resolve messages by string key or re-implement `{param}` interpolation. A key-string resolver turns a missing key back into silent runtime text, surrendering the type safety that is the entire reason to use Paraglide.
217
+ - Don't reach for i18next/react-i18next or a runtime-fetch translation loader — Paraglide's compiled functions are the whole delivery mechanism.
218
+ - Don't tokenize backend error messages or logs here — those are not frontend display strings.
@@ -1,12 +1,6 @@
1
- ---
2
- name: pikku-rtl
3
- description: 'Make a Pikku frontend work in both English (LTR) and Arabic / right-to-left languages. Direction is derived from the active locale, applied once at the document root, and the layout mirrors itself — but only if styling is written flow-relative (margin-inline-start, text-align: start, Mantine ms/me) instead of left/right. TRIGGER when: adding Arabic (or Hebrew/Farsi/Urdu), asked to "support RTL / right-to-left / bidi / mirror the layout", or writing layout styles in an app that may run RTL. Builds on pikku-i18n (an RTL language is just another locale file). DO NOT TRIGGER for backend functions or for LTR-only copy changes.'
4
- installGroups: [client]
5
- ---
6
-
7
1
  # Pikku RTL (Arabic + English)
8
2
 
9
- This skill sits **on top of** `pikku-i18n`. That skill compiles a locale's
3
+ This reference sits **on top of** `references/messages.md`. That one compiles a locale's
10
4
  messages into typed `m.*()` functions; this one adds the second axis: a locale
11
5
  also has a **direction**. Arabic is not special-cased — it is just another
12
6
  `messages/ar.json` listed in `project.inlang/settings.json`, plus the document
@@ -23,7 +17,7 @@ _layout_ code — directional icons still need one manual step, covered below.
23
17
  ## Agent Operating Procedure
24
18
 
25
19
  1. **Messages first.** Every visible string is already an `m.*()` message via
26
- `pikku-i18n`. Arabic copy goes in `messages/ar.json`, mirroring `en.json`'s
20
+ `references/messages.md`. Arabic copy goes in `messages/ar.json`, mirroring `en.json`'s
27
21
  keys with the `{param}` names kept identical.
28
22
  2. **Add the direction helper** to the i18n config (one home for locale→dir):
29
23
  ```ts
@@ -219,5 +213,5 @@ left` with the flow-relative equivalent; revert any manual `row-reverse`.
219
213
  - Don't set `dir` on individual components — it belongs on `<html>` so the whole
220
214
  document (and Mantine) agrees.
221
215
  - Don't translate Arabic copy outside the message system; an RTL language is a
222
- normal locale, governed by `pikku-i18n`. There is no `t()` and no i18next in a
216
+ normal locale, governed by `references/messages.md`. There is no `t()` and no i18next in a
223
217
  Pikku frontend — the string comes from `m.some__key()`.
@@ -13,6 +13,7 @@ description: >-
13
13
  brief to record. DO NOT TRIGGER when: user asks what
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
+ installGroups: [core]
16
17
  ---
17
18
 
18
19
  # Pikku Knowledge
@@ -242,6 +243,20 @@ pikku knowledge index --check # report stale indexes without writing (CI gate)
242
243
 
243
244
  `index` rewrites only the block between `<!-- pikku:knowledge-index -->` markers, creating a scaffolded `index.md` for a section that has none. It is idempotent — running it twice changes nothing.
244
245
 
246
+ ### The milestone plan
247
+
248
+ A milestone note says what the app must DO. Its **plan** — JSON beside the note, not prose — says what has to exist for it, and is what a finished build is measured against:
249
+
250
+ ```bash
251
+ pikku knowledge plan schema # the format, in full
252
+ pikku knowledge plan set <milestone> <file> # validate and write it
253
+ pikku knowledge plan show <milestone> --for-build # the ordered work a build follows
254
+ pikku knowledge plan progress <milestone> # what it still owes, read from .pikku/
255
+ pikku knowledge plan defer <milestone> <item> -r "<why>"
256
+ ```
257
+
258
+ `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`.
259
+
245
260
  ## Profiles built on this one
246
261
 
247
262
  OKF permits frontmatter fields a reader does not know, and the parser ignores them rather than failing. That is the extension point: a tool layered on Pikku can add its own sections and fields on top of everything above without forking the format.