@pikku/skills 0.12.13 → 0.12.15

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 (49) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +1 -1
  4. package/skills/pikku-ai-vercel/SKILL.md +1 -1
  5. package/skills/pikku-ai-voice/SKILL.md +1 -0
  6. package/skills/pikku-audit/SKILL.md +0 -1
  7. package/skills/pikku-better-auth/SKILL.md +1 -1
  8. package/skills/pikku-build-app/SKILL.md +0 -1
  9. package/skills/pikku-build-platform/SKILL.md +3 -4
  10. package/skills/pikku-build-quick/SKILL.md +0 -1
  11. package/skills/pikku-cli/SKILL.md +1 -1
  12. package/skills/pikku-concepts/SKILL.md +58 -16
  13. package/skills/pikku-concepts/references/concept-mapping.md +1 -1
  14. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -1
  15. package/skills/pikku-deps/SKILL.md +0 -1
  16. package/skills/pikku-emails/SKILL.md +1 -1
  17. package/skills/pikku-fabric/SKILL.md +55 -4
  18. package/skills/pikku-feature/SKILL.md +0 -1
  19. package/skills/pikku-gateway-slack/SKILL.md +1 -0
  20. package/skills/pikku-i18n/SKILL.md +1 -1
  21. package/skills/pikku-jose/SKILL.md +1 -0
  22. package/skills/pikku-knowledge/SKILL.md +0 -1
  23. package/skills/pikku-kysely/SKILL.md +1 -1
  24. package/skills/pikku-machine-auth/SKILL.md +1 -0
  25. package/skills/pikku-meta/SKILL.md +139 -0
  26. package/skills/pikku-n8n-import/SKILL.md +1 -0
  27. package/skills/pikku-paraglide/SKILL.md +1 -1
  28. package/skills/pikku-product-second-opinion/SKILL.md +0 -1
  29. package/skills/pikku-queue/SKILL.md +1 -1
  30. package/skills/pikku-react/SKILL.md +1 -1
  31. package/skills/pikku-react-query/SKILL.md +1 -1
  32. package/skills/pikku-realtime/SKILL.md +0 -1
  33. package/skills/pikku-rpc/SKILL.md +1 -1
  34. package/skills/pikku-rtl/SKILL.md +1 -1
  35. package/skills/pikku-scenario/SKILL.md +29 -2
  36. package/skills/pikku-schedule/SKILL.md +178 -50
  37. package/skills/pikku-schema-ajv/SKILL.md +0 -1
  38. package/skills/pikku-schema-cfworker/SKILL.md +0 -1
  39. package/skills/pikku-security/SKILL.md +2 -2
  40. package/skills/pikku-software-archaeology/SKILL.md +0 -1
  41. package/skills/pikku-template-clone/SKILL.md +0 -1
  42. package/skills/pikku-trigger/SKILL.md +1 -1
  43. package/skills/pikku-versioning/SKILL.md +0 -1
  44. package/skills/pikku-workflow/SKILL.md +1 -1
  45. package/skills/pikku-workflows-client/SKILL.md +1 -1
  46. package/skills/pikku-ws/SKILL.md +1 -0
  47. package/skills/pikku-cron/SKILL.md +0 -221
  48. package/skills/pikku-info/SKILL.md +0 -110
  49. package/skills/pikku-tag-middleware/SKILL.md +0 -14
@@ -534,6 +534,32 @@ export const opensTheCart = pikkuScenarioStep<
534
534
  - `pikku scenario run <env> --no-browser` **skips** scenarios containing browser steps and reports them as skipped — it does not fail them. That is how a machine with no browser stays green.
535
535
  - Playwright auto-waits; do not wrap `page.click` in `expectEventually`.
536
536
 
537
+ #### Locate by message key, never by rendered copy
538
+
539
+ If the app is translated, **no step may contain a user-visible string.** `getByLabel('Full Name')` passes only while the browser happens to render the base locale, and any copy edit turns it into a selector timeout that points at the wizard rather than at the rename that caused it — the test looks broken where it is merely stale.
540
+
541
+ The message catalogue already holds the string under a key. Read it from there. Type the lookup off the catalogue JSON so a renamed or misspelled key is a **compile** error rather than a run-time timeout:
542
+
543
+ ```typescript
544
+ // tests/scenarios/i18n.ts
545
+ import type messages from '../../../../apps/web/messages/en.json'
546
+
547
+ export type MessageKey = keyof typeof messages
548
+
549
+ export const t = (key: MessageKey, locale = baseLocale): string => { /* … */ }
550
+ ```
551
+
552
+ ```typescript
553
+ await page.getByLabel(t('jobs_apply_fullname')).fill(identity.name)
554
+ await page.getByRole('button', { name: t('jobs_apply_submit'), exact: true }).click()
555
+ ```
556
+
557
+ - Type off `messages/<baseLocale>.json`, **not** the generated Paraglide output — `i18n/paraglide/` is build output, so typing against it makes the tests unbuildable until the app has been built. The JSON is the tracked source.
558
+ - Fall back to the base locale for a key a locale has not translated. That is what Paraglide does at run time, so a helper that throws instead would disagree with the screen the test is looking at.
559
+ - This is not only about locators. A copy literal passed to a **project helper** (`pick('Where would you like to work?', …)`) reaches the DOM the same way, and so does a pane name quoted back in a failure message. `pikku fabric validate` scans every string in a `*.steps.ts` / `*.scenario.ts` against the base catalogue and errors on any verbatim match, wherever it sits.
560
+ - A regex locator (`{ name: /^Next$/i }`) hides the literal but not the problem. `{ name: t('key'), exact: true }` is both stricter and locale-correct.
561
+ - Strings the catalogue does not own — a test id, a fixture filename, a seeded value — stay literal. The catalogue is the test for whether something is copy.
562
+
537
563
  ## Configuration
538
564
 
539
565
  Personas, actors and environments live in `pikku.config.json`:
@@ -628,7 +654,7 @@ An actor with no `persona` is its own persona, so a project that never declares
628
654
  ### The same actors sign a human in
629
655
 
630
656
  Declared actors are not only for automated runs. `signInPath` is Better Auth's
631
- `actor` plugin (see `pikku-better-auth`), which any caller can post to — so the
657
+ `actor` plugin (see `pikku-better-auth`, a separate install), which any caller can post to — so the
632
658
  frontend gets a one-click "Sign in as …" switcher over the **same** list, and an
633
659
  app can be reviewed as each kind of user without anyone knowing a seed password.
634
660
 
@@ -639,7 +665,7 @@ control renders nothing there — but gate the reads on your bundler's dev flag
639
665
  anyway (`import.meta.env.DEV ? … : undefined`) so the secret never reaches a
640
666
  production bundle in the first place.
641
667
 
642
- Do not hand-roll the switcher: `useDevActors()` (`pikku-react`) is the logic and
668
+ Do not hand-roll the switcher: `useDevActors()` (`pikku-react`, a separate install) is the logic and
643
669
  `<DevActorSwitcher />` from `@pikku/mantine/dev` is a ready rendering of it.
644
670
  `pikku fabric validate` **requires** any frontend with a login screen to ship
645
671
  one — without it a reviewer is locked out of their own sandbox.
@@ -758,6 +784,7 @@ Services are plain objects — a Pikku function is pure business logic, so a moc
758
784
  | `sleep()` before asserting | Use `expectEventually`. |
759
785
  | A step named `clicksAddToBasket` / `opensThePage` | That is an action, not an intent. Name the step for what the actor wanted; put the clicking in a utility. |
760
786
  | A browser step that assumes it is already on a page | It can then only run mid-flow. Arrive first — check the URL, navigate if needed. |
787
+ | `getByLabel('Full Name')` in a translated app | Passes only in the base locale, and a copy edit breaks it as an unexplained timeout. Locate by message key. |
761
788
  | A `browser` binding guarding `if (!browser)` | The binding guarantees it. The guard hides the real error, which is a missing actor (`PKU677`). |
762
789
  | A step with a `func:` instead of a surface binding | There is no `func` on a step. Bodies live under `default` / `browser` / `cli`; a step with none throws at load. |
763
790
  | `expectEventually` in a `pikkuWorkflowFunc` | `PKU675` — scenario-only. |
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: pikku-schedule
3
3
  description: >-
4
- Use when setting up in-memory cron scheduling in a Pikku app. Covers InMemorySchedulerService
5
- for running scheduled tasks. TRIGGER when: code uses InMemorySchedulerService,
6
- PikkuTaskScheduler, or user asks about in-memory scheduling, cron jobs without external
7
- dependencies, or @pikku/schedule. DO NOT TRIGGER when: user asks about cron wiring (use
8
- pikku-cron) or queue-based scheduling with BullMQ/PgBoss (use pikku-queue).
4
+ Use when adding scheduled tasks, recurring jobs, or cron-based automation to a Pikku app. Covers
5
+ wireScheduler, cron expressions, the scheduled task wire object, and scheduler middleware.
6
+ TRIGGER when: code uses wireScheduler, user asks about cron, scheduled tasks, recurring jobs, or
7
+ "run every X minutes/hours". DO NOT TRIGGER when: user asks about background jobs with retries
8
+ (use pikku-queue) or event-driven triggers (use pikku-trigger).
9
9
  installGroups: [core]
10
10
  ---
11
11
 
12
- # Pikku Schedule (In-Memory Scheduler)
12
+ # Pikku Scheduled Tasks
13
13
 
14
14
  ## Agent Operating Procedure
15
15
 
@@ -21,75 +21,203 @@ Use this skill as an execution checklist, not reference material.
21
21
  4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
22
22
  5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
23
23
 
24
- `@pikku/schedule` provides an in-memory cron scheduler for running Pikku scheduled functions without external dependencies like Redis or PostgreSQL.
24
+ Wire Pikku functions to run on a schedule using cron expressions. Uses `pikkuVoidFunc` (no input/output).
25
25
 
26
- ## Installation
26
+ `pikku dev`, `pikku serve` and the standalone deploy adapter each register a scheduler service for you, so a wired task runs without any setup. Only register one yourself when deploying somewhere those do not reach, and then take it off the queue factory (`bullFactory.getSchedulerService()`, `pgBossFactory.getSchedulerService()` — see `pikku-queue`) so it survives a restart and is shared between instances.
27
+
28
+ ## Before You Start
27
29
 
28
30
  ```bash
29
- yarn add @pikku/schedule
31
+ pikku info functions --verbose # See existing functions and their types
32
+ pikku info tags --verbose # Understand project organization
30
33
  ```
31
34
 
35
+ See `pikku-concepts` for the core mental model.
36
+
32
37
  ## API Reference
33
38
 
34
- ### `InMemorySchedulerService`
39
+ ### `wireScheduler(config)`
35
40
 
36
41
  ```typescript
37
- import { InMemorySchedulerService } from '@pikku/schedule'
38
-
39
- const schedulerService = new InMemorySchedulerService()
40
- await schedulerService.start() // registers a CronJob per wired scheduled task
42
+ import { wireScheduler } from '@pikku/core/scheduler'
43
+
44
+ wireScheduler({
45
+ name: string, // Unique scheduler name
46
+ schedule: string, // Cron expression
47
+ func: PikkuVoidFunc, // Must be pikkuVoidFunc (no input/output)
48
+ tags?: string[], // Targets tag middleware — see pikku-middleware
49
+ middleware?: PikkuMiddleware[],
50
+ })
41
51
  ```
42
52
 
43
- It implements core's `SchedulerService` on two mechanisms: `cron` for the
44
- recurring tasks you declared with `wireScheduler` (see `pikku-cron`), and
45
- `setTimeout` for one-off delayed RPCs. Both live in process memory, so nothing
46
- survives a restart and nothing is shared between instances — fine for
47
- development and a single-instance deployment, wrong for anything else.
48
-
49
- `PikkuTaskScheduler` is a deprecated alias for the same class.
53
+ ### Wire Object (`wire.scheduledTask`)
50
54
 
51
- ### Scheduling a one-off RPC
55
+ Inside scheduled functions:
52
56
 
53
57
  ```typescript
54
- const taskId = await schedulerService.scheduleRPC(
55
- '5m',
56
- 'sendReminder',
57
- data,
58
- session
59
- )
60
- await schedulerService.getTask(taskId) // { rpcName, scheduledFor, status, … } | null
61
- await schedulerService.getAllTasks() // pending one-offs only
62
- await schedulerService.unschedule(taskId) // true when it was still pending
58
+ wire.scheduledTask.name // Scheduler name
59
+ wire.scheduledTask.schedule // Cron expression string
60
+ wire.scheduledTask.executionTime // Date this execution was triggered
61
+ wire.scheduledTask.skip(reason?) // Abort this execution — THROWS, never returns
62
+ ```
63
+
64
+ **`skip()` aborts by throwing.** It reads like an early return but it is not:
65
+ nothing after the call runs, so there is no need to `return` afterwards. The
66
+ consequence that bites is in middleware — a `try/catch` around `await next()`
67
+ will catch a skip and report it as a failure. If your middleware distinguishes
68
+ success from failure, let the skip pass through rather than logging it as an
69
+ error.
70
+
71
+ ### Cron Expression Reference
72
+
73
+ ```
74
+ ┌───────────── minute (0-59)
75
+ │ ┌───────────── hour (0-23)
76
+ │ │ ┌───────────── day of month (1-31)
77
+ │ │ │ ┌───────────── month (1-12)
78
+ │ │ │ │ ┌───────────── day of week (0-7, 0 and 7 = Sunday)
79
+ │ │ │ │ │
80
+ * * * * *
63
81
  ```
64
82
 
65
- The delay is milliseconds or a duration string (`'30s'`, `'5m'`, `'2h'`). This is
66
- also the mechanism a workflow's delayed steps use, which is why a workflow that
67
- sleeps needs a `schedulerService` registered.
83
+ Common patterns:
84
+
85
+ | Expression | Meaning |
86
+ | ------------- | -------------------------- |
87
+ | `*/5 * * * *` | Every 5 minutes |
88
+ | `0 9 * * *` | Daily at 9:00 AM |
89
+ | `0 9 * * 1` | Every Monday at 9:00 AM |
90
+ | `0 0 1 * *` | First of month at midnight |
91
+ | `0 */6 * * *` | Every 6 hours |
92
+ | `30 2 * * 0` | Sundays at 2:30 AM |
68
93
 
69
94
  ## Usage Patterns
70
95
 
71
- ### Basic Setup
96
+ ### Basic Scheduled Task
97
+
98
+ ```typescript
99
+ const dailySummary = pikkuVoidFunc({
100
+ title: 'Daily Summary',
101
+ func: async ({ db, emailService, logger }) => {
102
+ logger.info('Generating daily summary')
103
+ const stats = await db.getDailyStats()
104
+ await emailService.sendSummary(stats)
105
+ },
106
+ })
107
+
108
+ wireScheduler({
109
+ name: 'dailySummary',
110
+ schedule: '0 9 * * *',
111
+ func: dailySummary,
112
+ })
113
+ ```
72
114
 
73
- The scheduler is a singleton service under the name **`schedulerService`**, and
74
- it is started in your server bootstrap — declaring it without calling `start()`
75
- registers no cron jobs, so nothing ever fires:
115
+ ### Using the Wire Object
76
116
 
77
117
  ```typescript
78
- // start.ts
79
- import { InMemorySchedulerService } from '@pikku/schedule'
118
+ const weeklyCleanup = pikkuVoidFunc({
119
+ title: 'Weekly Cleanup',
120
+ func: async ({ db, logger }, _input, wire) => {
121
+ logger.info(`Running: ${wire.scheduledTask.name}`)
122
+ logger.info(`Schedule: ${wire.scheduledTask.schedule}`)
123
+ logger.info(`Execution time: ${wire.scheduledTask.executionTime}`)
124
+
125
+ const staleCount = await db.countStaleTodos()
126
+ if (staleCount === 0) {
127
+ wire.scheduledTask.skip('No stale todos found') // throws — nothing below runs
128
+ }
129
+
130
+ await db.deleteCompletedTodos({ olderThan: '30d' })
131
+ logger.info(`Cleaned ${staleCount} stale todos`)
132
+ },
133
+ })
80
134
 
81
- const schedulerService = new InMemorySchedulerService()
82
- const singletonServices = await createSingletonServices(config, {
83
- schedulerService,
135
+ wireScheduler({
136
+ name: 'weeklyCleanup',
137
+ schedule: '0 0 * * 0',
138
+ func: weeklyCleanup,
84
139
  })
140
+ ```
141
+
142
+ ### Scheduler Middleware
85
143
 
86
- await appServer.start()
87
- await schedulerService.start()
144
+ ```typescript
145
+ const schedulerMetrics = pikkuMiddleware(
146
+ async ({ logger }, { scheduledTask }, next) => {
147
+ const start = Date.now()
148
+ logger.info(`Task started: ${scheduledTask.name}`)
149
+
150
+ try {
151
+ await next()
152
+ logger.info(`Task completed: ${scheduledTask.name}`, {
153
+ duration: Date.now() - start,
154
+ })
155
+ } catch (error) {
156
+ logger.error(`Task failed: ${scheduledTask.name}`, {
157
+ error: error.message,
158
+ duration: Date.now() - start,
159
+ })
160
+ throw error
161
+ }
162
+ }
163
+ )
164
+
165
+ wireScheduler({
166
+ name: 'dailySummary',
167
+ schedule: '0 9 * * *',
168
+ func: dailySummary,
169
+ middleware: [schedulerMetrics],
170
+ })
88
171
  ```
89
172
 
90
- Call `close()` on shutdown — it stops every cron job and clears pending timers.
173
+ ## Complete Example
174
+
175
+ ```typescript
176
+ // functions/scheduled.functions.ts
177
+ export const dailySummary = pikkuVoidFunc({
178
+ title: 'Daily Summary',
179
+ func: async ({ db, emailService, logger }) => {
180
+ const stats = await db.getDailyStats()
181
+ await emailService.sendSummary(stats)
182
+ logger.info('Daily summary sent', { stats })
183
+ },
184
+ })
185
+
186
+ export const cleanupExpired = pikkuVoidFunc({
187
+ title: 'Cleanup Expired',
188
+ func: async ({ db, logger }, _input, wire) => {
189
+ const count = await db.countExpiredSessions()
190
+ if (count === 0) {
191
+ wire.scheduledTask.skip('No expired sessions') // throws — nothing below runs
192
+ }
193
+ await db.deleteExpiredSessions()
194
+ logger.info(`Cleaned ${count} expired sessions`)
195
+ },
196
+ })
197
+
198
+ export const syncInventory = pikkuVoidFunc({
199
+ title: 'Sync Inventory',
200
+ func: async ({ inventoryApi, db, logger }) => {
201
+ const updates = await inventoryApi.getChanges()
202
+ await db.applyInventoryUpdates(updates)
203
+ logger.info(`Synced ${updates.length} inventory changes`)
204
+ },
205
+ })
91
206
 
92
- For distributed or persistent scheduling, take the scheduler service off the
93
- queue factory instead (`bullFactory.getSchedulerService()`,
94
- `pgBossFactory.getSchedulerService()`) and register it under the same name. See
95
- `pikku-queue`.
207
+ // wirings/scheduler.wiring.ts
208
+ wireScheduler({
209
+ name: 'dailySummary',
210
+ schedule: '0 9 * * *',
211
+ func: dailySummary,
212
+ })
213
+ wireScheduler({
214
+ name: 'cleanupExpired',
215
+ schedule: '0 */6 * * *',
216
+ func: cleanupExpired,
217
+ })
218
+ wireScheduler({
219
+ name: 'syncInventory',
220
+ schedule: '*/15 * * * *',
221
+ func: syncInventory,
222
+ })
223
+ ```
@@ -5,7 +5,6 @@ description: >-
5
5
  request/response validation. TRIGGER when: code uses AjvSchemaService, user asks about AJV, JSON
6
6
  schema validation, or @pikku/schema-ajv. DO NOT TRIGGER when: user asks about Cloudflare Workers
7
7
  schema validation (use pikku-schema-cfworker).
8
- installGroups: [core]
9
8
  ---
10
9
 
11
10
  # Pikku Schema AJV (JSON Schema Validation)
@@ -6,7 +6,6 @@ description: >-
6
6
  CFWorkerSchemaService, user asks about schema validation on Cloudflare Workers, or
7
7
  @pikku/schema-cfworker. DO NOT TRIGGER when: user asks about AJV schema validation (use
8
8
  pikku-schema-ajv).
9
- installGroups: [core, fabric]
10
9
  ---
11
10
 
12
11
  # Pikku Schema CFWorker (Cloudflare Workers Validation)
@@ -61,7 +61,7 @@ with the default `auth` would be rejected before its body ever ran.
61
61
  Apply these via `addHTTPMiddleware` in a wirings file:
62
62
 
63
63
  ```typescript
64
- import { authBearer, authCookie, authAPIKey } from '@pikku/core/middleware'
64
+ import { authBearer, authCookie, authAPIKey } from '#pikku/middleware'
65
65
  import { addHTTPMiddleware } from '#pikku/http'
66
66
 
67
67
  // JWT bearer token — reads Authorization header
@@ -128,7 +128,7 @@ export const isVerified = pikkuAuth(
128
128
  )
129
129
 
130
130
  // wirings/auth.wiring.ts
131
- import { authCookie } from '@pikku/core/middleware'
131
+ import { authCookie } from '#pikku/middleware'
132
132
  import { addHTTPMiddleware } from '#pikku/http'
133
133
 
134
134
  addHTTPMiddleware('*', [
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: pikku-software-archaeology
3
3
  description: 'Use when reverse-engineering an existing repository into a Product Blueprint — recovering what product an undocumented or organically-grown codebase implements so it can be rebuilt cleanly (e.g. as a Pikku app). TRIGGER when: user says "extract a blueprint", "reverse engineer this app", "what does this codebase actually do as a product", "prepare this repo for a rewrite/migration", or points at a legacy repo (any language — JS, TS, Ruby, Python, PHP, Go) and asks for its domains, workflows, business rules, or a rebuild plan. DO NOT TRIGGER for: documenting code structure, generating API docs from an already-clean codebase, or code review.'
4
- installGroups: [fabric]
5
4
  ---
6
5
 
7
6
  # Software Archaeology
@@ -2,7 +2,6 @@
2
2
  name: pikku-template-clone
3
3
  description: 'Standard cleanup to run right after a Pikku template is cloned or scaffolded into a new project. TRIGGER when: a Pikku template was just cloned/scaffolded (via `npm create pikku`, `git clone <template>`, or the user says "I cloned the kanban template / starter / template"), or the working tree still looks like an untouched template (template README, placeholder `@project/*` name in package.json). DO NOT TRIGGER when: working in an established project mid-feature, or editing the template repo itself.'
4
4
  allowed-tools: Bash(git status *), Bash(git add *), Bash(git commit *), Bash(git rm *), Bash(git mv *), Bash(git log *)
5
- installGroups: [core]
6
5
  ---
7
6
 
8
7
  # Pikku Template Post-Clone Cleanup
@@ -5,7 +5,7 @@ description: >-
5
5
  PostgreSQL LISTEN/NOTIFY, or custom event sources. Covers wireTrigger, wireTriggerSource, and
6
6
  pikkuTriggerFunc. TRIGGER when: code uses wireTrigger/wireTriggerSource/pikkuTriggerFunc, user
7
7
  asks about event-driven functions, Redis pub/sub, PostgreSQL LISTEN/NOTIFY, or reacting to
8
- external events. DO NOT TRIGGER when: user asks about scheduled tasks (use pikku-cron) or
8
+ external events. DO NOT TRIGGER when: user asks about scheduled tasks (use pikku-schedule) or
9
9
  background job queues (use pikku-queue).
10
10
  installGroups: [core]
11
11
  ---
@@ -11,7 +11,6 @@ description: >-
11
11
  CLI commands. DO NOT TRIGGER when: user asks about secrets/variables/OAuth2 (use pikku-config)
12
12
  or general function definitions (use pikku-concepts), or about updating dependency versions
13
13
  (use pikku-deps).
14
- installGroups: [core]
15
14
  ---
16
15
 
17
16
  # Pikku Function Versioning
@@ -6,7 +6,7 @@ description: >-
6
6
  TRIGGER when: code uses pikkuWorkflowFunc/pikkuWorkflowGraph, user asks about workflows,
7
7
  multi-step processes, durable execution, suspend/resume, or DAG orchestration. DO NOT TRIGGER
8
8
  when: user asks about simple background jobs (use pikku-queue) or scheduled tasks (use
9
- pikku-cron).
9
+ pikku-schedule).
10
10
  installGroups: [core]
11
11
  ---
12
12
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: pikku-workflows-client
3
3
  description: 'Run Pikku workflows from a React frontend and track their progress. Covers `useRunWorkflow` (run-and-wait), `useStartWorkflow` (fire-and-poll), and `useWorkflowStatus` (live status). TRIGGER when: a React component needs to invoke or display the status of a Pikku workflow, the user mentions long-running tasks / background jobs / progress UI tied to a workflow, or asks how to start/track a workflow from the client. DO NOT TRIGGER when: the user is wiring the workflow itself (use pikku-workflow) or only making regular RPC calls (use pikku-react-query).'
4
- installGroups: [core]
4
+ installGroups: [client]
5
5
  ---
6
6
 
7
7
  # Pikku Workflows — Client Hooks
@@ -5,6 +5,7 @@ description: >-
5
5
  adapter for Pikku channels. TRIGGER when: code uses @pikku/ws, user asks about ws library
6
6
  WebSocket server, or Node.js WebSocket runtime. DO NOT TRIGGER when: user asks about WebSocket
7
7
  wiring/channels (use pikku-websocket) or uWebSockets (use pikku-deploy-uws).
8
+ installGroups: [fabric]
8
9
  ---
9
10
 
10
11
  # Pikku WS (WebSocket Server Runtime)
@@ -1,221 +0,0 @@
1
- ---
2
- name: pikku-cron
3
- description: >-
4
- Use when adding scheduled tasks, recurring jobs, or cron-based automation to a Pikku app. Covers
5
- wireScheduler, cron expressions, scheduled task wire object, and scheduler middleware. TRIGGER
6
- when: code uses wireScheduler, user asks about cron, scheduled tasks, recurring jobs, or "run
7
- every X minutes/hours". DO NOT TRIGGER when: user asks about background jobs with retries (use
8
- pikku-queue) or event-driven triggers (use pikku-trigger).
9
- installGroups: [core]
10
- ---
11
-
12
- # Pikku Cron/Scheduler Wiring
13
-
14
- ## Agent Operating Procedure
15
-
16
- Use this skill as an execution checklist, not reference material.
17
-
18
- 1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
19
- 2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
20
- 3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
21
- 4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
22
- 5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
23
-
24
- Wire Pikku functions to run on a schedule using cron expressions. Uses `pikkuVoidFunc` (no input/output).
25
-
26
- ## Before You Start
27
-
28
- ```bash
29
- pikku info functions --verbose # See existing functions and their types
30
- pikku info tags --verbose # Understand project organization
31
- ```
32
-
33
- See `pikku-concepts` for the core mental model.
34
-
35
- ## API Reference
36
-
37
- ### `wireScheduler(config)`
38
-
39
- ```typescript
40
- import { wireScheduler } from '@pikku/core/scheduler'
41
-
42
- wireScheduler({
43
- name: string, // Unique scheduler name
44
- schedule: string, // Cron expression
45
- func: PikkuVoidFunc, // Must be pikkuVoidFunc (no input/output)
46
- tags?: string[], // Targets tag middleware — see pikku-middleware
47
- middleware?: PikkuMiddleware[],
48
- })
49
- ```
50
-
51
- ### Wire Object (`wire.scheduledTask`)
52
-
53
- Inside scheduled functions:
54
-
55
- ```typescript
56
- wire.scheduledTask.name // Scheduler name
57
- wire.scheduledTask.schedule // Cron expression string
58
- wire.scheduledTask.executionTime // Date this execution was triggered
59
- wire.scheduledTask.skip(reason?) // Abort this execution — THROWS, never returns
60
- ```
61
-
62
- **`skip()` aborts by throwing.** It reads like an early return but it is not:
63
- nothing after the call runs, so there is no need to `return` afterwards. The
64
- consequence that bites is in middleware — a `try/catch` around `await next()`
65
- will catch a skip and report it as a failure. If your middleware distinguishes
66
- success from failure, let the skip pass through rather than logging it as an
67
- error.
68
-
69
- ### Cron Expression Reference
70
-
71
- ```
72
- ┌───────────── minute (0-59)
73
- │ ┌───────────── hour (0-23)
74
- │ │ ┌───────────── day of month (1-31)
75
- │ │ │ ┌───────────── month (1-12)
76
- │ │ │ │ ┌───────────── day of week (0-7, 0 and 7 = Sunday)
77
- │ │ │ │ │
78
- * * * * *
79
- ```
80
-
81
- Common patterns:
82
-
83
- | Expression | Meaning |
84
- | ------------- | -------------------------- |
85
- | `*/5 * * * *` | Every 5 minutes |
86
- | `0 9 * * *` | Daily at 9:00 AM |
87
- | `0 9 * * 1` | Every Monday at 9:00 AM |
88
- | `0 0 1 * *` | First of month at midnight |
89
- | `0 */6 * * *` | Every 6 hours |
90
- | `30 2 * * 0` | Sundays at 2:30 AM |
91
-
92
- ## Usage Patterns
93
-
94
- ### Basic Scheduled Task
95
-
96
- ```typescript
97
- const dailySummary = pikkuVoidFunc({
98
- title: 'Daily Summary',
99
- func: async ({ db, emailService, logger }) => {
100
- logger.info('Generating daily summary')
101
- const stats = await db.getDailyStats()
102
- await emailService.sendSummary(stats)
103
- },
104
- })
105
-
106
- wireScheduler({
107
- name: 'dailySummary',
108
- schedule: '0 9 * * *',
109
- func: dailySummary,
110
- })
111
- ```
112
-
113
- ### Using the Wire Object
114
-
115
- ```typescript
116
- const weeklyCleanup = pikkuVoidFunc({
117
- title: 'Weekly Cleanup',
118
- func: async ({ db, logger }, _input, wire) => {
119
- logger.info(`Running: ${wire.scheduledTask.name}`)
120
- logger.info(`Schedule: ${wire.scheduledTask.schedule}`)
121
- logger.info(`Execution time: ${wire.scheduledTask.executionTime}`)
122
-
123
- const staleCount = await db.countStaleTodos()
124
- if (staleCount === 0) {
125
- wire.scheduledTask.skip('No stale todos found') // throws — nothing below runs
126
- }
127
-
128
- await db.deleteCompletedTodos({ olderThan: '30d' })
129
- logger.info(`Cleaned ${staleCount} stale todos`)
130
- },
131
- })
132
-
133
- wireScheduler({
134
- name: 'weeklyCleanup',
135
- schedule: '0 0 * * 0',
136
- func: weeklyCleanup,
137
- })
138
- ```
139
-
140
- ### Scheduler Middleware
141
-
142
- ```typescript
143
- const schedulerMetrics = pikkuMiddleware(
144
- async ({ logger }, { scheduledTask }, next) => {
145
- const start = Date.now()
146
- logger.info(`Task started: ${scheduledTask.name}`)
147
-
148
- try {
149
- await next()
150
- logger.info(`Task completed: ${scheduledTask.name}`, {
151
- duration: Date.now() - start,
152
- })
153
- } catch (error) {
154
- logger.error(`Task failed: ${scheduledTask.name}`, {
155
- error: error.message,
156
- duration: Date.now() - start,
157
- })
158
- throw error
159
- }
160
- }
161
- )
162
-
163
- wireScheduler({
164
- name: 'dailySummary',
165
- schedule: '0 9 * * *',
166
- func: dailySummary,
167
- middleware: [schedulerMetrics],
168
- })
169
- ```
170
-
171
- ## Complete Example
172
-
173
- ```typescript
174
- // functions/scheduled.functions.ts
175
- export const dailySummary = pikkuVoidFunc({
176
- title: 'Daily Summary',
177
- func: async ({ db, emailService, logger }) => {
178
- const stats = await db.getDailyStats()
179
- await emailService.sendSummary(stats)
180
- logger.info('Daily summary sent', { stats })
181
- },
182
- })
183
-
184
- export const cleanupExpired = pikkuVoidFunc({
185
- title: 'Cleanup Expired',
186
- func: async ({ db, logger }, _input, wire) => {
187
- const count = await db.countExpiredSessions()
188
- if (count === 0) {
189
- wire.scheduledTask.skip('No expired sessions') // throws — nothing below runs
190
- }
191
- await db.deleteExpiredSessions()
192
- logger.info(`Cleaned ${count} expired sessions`)
193
- },
194
- })
195
-
196
- export const syncInventory = pikkuVoidFunc({
197
- title: 'Sync Inventory',
198
- func: async ({ inventoryApi, db, logger }) => {
199
- const updates = await inventoryApi.getChanges()
200
- await db.applyInventoryUpdates(updates)
201
- logger.info(`Synced ${updates.length} inventory changes`)
202
- },
203
- })
204
-
205
- // wirings/scheduler.wiring.ts
206
- wireScheduler({
207
- name: 'dailySummary',
208
- schedule: '0 9 * * *',
209
- func: dailySummary,
210
- })
211
- wireScheduler({
212
- name: 'cleanupExpired',
213
- schedule: '0 */6 * * *',
214
- func: cleanupExpired,
215
- })
216
- wireScheduler({
217
- name: 'syncInventory',
218
- schedule: '*/15 * * * *',
219
- func: syncInventory,
220
- })
221
- ```