@pikku/skills 0.12.22 → 0.12.25
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/CHANGELOG.md +101 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +2 -2
- package/skills/pikku-agent/SKILL.md +67 -316
- package/skills/pikku-agent/references/agents.md +299 -0
- package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
- package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
- package/skills/pikku-architect/SKILL.md +264 -0
- package/skills/pikku-auth/SKILL.md +89 -0
- package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +1 -27
- package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
- package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
- package/skills/{pikku-permissions/SKILL.md → pikku-auth/references/permissions.md} +1 -20
- package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +0 -20
- package/skills/pikku-build/SKILL.md +87 -0
- package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +75 -23
- package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
- package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +1 -1
- package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
- package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
- package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +4 -20
- package/skills/{pikku-build-app → pikku-build}/references/ship.md +7 -1
- package/skills/pikku-concepts/SKILL.md +72 -7
- package/skills/pikku-concepts/references/concept-mapping.md +8 -8
- package/skills/pikku-deploy/SKILL.md +158 -0
- package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
- package/skills/pikku-deploy/references/cloudflare.md +104 -0
- package/skills/pikku-deploy/references/express.md +92 -0
- package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
- package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
- package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
- package/skills/pikku-deploy/references/uws.md +72 -0
- package/skills/pikku-deploy/references/ws.md +75 -0
- package/skills/pikku-emails/SKILL.md +3 -2
- package/skills/pikku-fabric/SKILL.md +12 -2
- package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
- package/skills/pikku-i18n/SKILL.md +60 -207
- package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
- package/skills/pikku-i18n/references/messages.md +218 -0
- package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
- package/skills/pikku-knowledge/SKILL.md +14 -0
- package/skills/pikku-kysely/SKILL.md +13 -13
- package/skills/pikku-meta/SKILL.md +58 -130
- package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
- package/skills/pikku-meta/references/meta.md +114 -0
- package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
- package/skills/pikku-middleware/SKILL.md +5 -5
- package/skills/pikku-n8n-import/SKILL.md +0 -1
- package/skills/pikku-react/SKILL.md +50 -298
- package/skills/pikku-react/references/client.md +293 -0
- package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
- package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
- package/skills/pikku-scenario/SKILL.md +60 -45
- package/skills/pikku-scenario/references/persona-run.md +148 -0
- package/skills/pikku-service-backends/SKILL.md +154 -0
- package/skills/pikku-service-backends/references/aws.md +106 -0
- package/skills/pikku-service-backends/references/backblaze.md +57 -0
- package/skills/pikku-service-backends/references/mongodb.md +90 -0
- package/skills/pikku-service-backends/references/redis.md +75 -0
- package/skills/pikku-service-backends/references/schema.md +63 -0
- package/skills/pikku-services/SKILL.md +68 -291
- package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
- package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
- package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
- package/skills/pikku-services/references/services.md +272 -0
- package/skills/pikku-software-archaeology/README.md +5 -1
- package/skills/pikku-software-archaeology/SKILL.md +15 -2
- package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
- package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
- package/skills/pikku-webhook/SKILL.md +199 -0
- package/skills/pikku-wiring/SKILL.md +180 -0
- package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
- package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
- package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
- package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +3 -39
- package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
- package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
- package/skills/{pikku-realtime/SKILL.md → pikku-wiring/references/realtime.md} +2 -25
- package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
- package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
- package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
- package/skills/pikku-workflow/SKILL.md +2 -2
- package/skills/pikku-aws/SKILL.md +0 -161
- package/skills/pikku-backblaze/SKILL.md +0 -104
- package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
- package/skills/pikku-deploy-express/SKILL.md +0 -122
- package/skills/pikku-deploy-uws/SKILL.md +0 -144
- package/skills/pikku-mongodb/SKILL.md +0 -113
- package/skills/pikku-product-second-opinion/README.md +0 -43
- package/skills/pikku-redis/SKILL.md +0 -99
- package/skills/pikku-schema-ajv/SKILL.md +0 -83
- package/skills/pikku-schema-cfworker/SKILL.md +0 -82
- package/skills/pikku-ws/SKILL.md +0 -87
- /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
- /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
- /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
- /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
- /package/skills/{pikku-realtime/references/other-routes.md → pikku-wiring/references/realtime-other-routes.md} +0 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Pikku AI Agent Wiring
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## Before You Start
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pikku info functions --verbose # See existing functions that can be used as agent tools
|
|
8
|
+
pikku info tags --verbose # Understand project organization
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
See `pikku-concepts` for the core mental model.
|
|
12
|
+
|
|
13
|
+
## API Reference
|
|
14
|
+
|
|
15
|
+
### `pikkuAgent(config)`
|
|
16
|
+
|
|
17
|
+
Import it from the generated agent types file — `#pikku` does not re-export it:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { pikkuAgent } from '#pikku/agent/pikku-agent-types.gen.js'
|
|
21
|
+
import { ref } from '#pikku/function'
|
|
22
|
+
|
|
23
|
+
pikkuAgent({
|
|
24
|
+
name: string, // Unique agent identifier
|
|
25
|
+
description: string, // What the agent does (shown in agent listings)
|
|
26
|
+
summary?: string,
|
|
27
|
+
errors?: string[],
|
|
28
|
+
|
|
29
|
+
// --- system prompt: three fields, joined role → personality → goal ---
|
|
30
|
+
role?: string, // Who it is: 'You are a support engineer triaging bugs.'
|
|
31
|
+
personality?: string, // How it sounds: tone, verbosity
|
|
32
|
+
goal: string, // REQUIRED — what it is for
|
|
33
|
+
|
|
34
|
+
model: string, // e.g. 'openai/gpt-5-mini'
|
|
35
|
+
temperature?: number,
|
|
36
|
+
providerOptions?: { // passed through untouched, keyed by provider
|
|
37
|
+
openai?: { reasoningEffort?: 'minimal' | ... },
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// --- capabilities: all three take ref() handles, not imported values ---
|
|
41
|
+
tools?: unknown[], // ref('todos:addTodo'), ref('graph:sleep'), …
|
|
42
|
+
agents?: unknown[], // sub-agents to delegate to
|
|
43
|
+
workflows?: unknown[], // workflows callable as a tool
|
|
44
|
+
agentMode?: 'delegate' | 'supervise',
|
|
45
|
+
|
|
46
|
+
memory?: {
|
|
47
|
+
storage?: string, // Service name for persistence (e.g. 'agentStorage')
|
|
48
|
+
vector?: string, // Vector store service name
|
|
49
|
+
embedder?: string, // Embedding service name
|
|
50
|
+
lastMessages?: number, // How many messages to retain in context
|
|
51
|
+
workingMemory?: ZodSchema, // Schema for structured working memory
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
maxSteps?: number, // Max tool-call rounds per invocation
|
|
55
|
+
toolChoice?: 'auto' | 'required' | 'none',
|
|
56
|
+
prepareStep?: (ctx) => void, // See "Narrowing tools per step"
|
|
57
|
+
input?: ZodSchema,
|
|
58
|
+
output?: ZodSchema, // Structured output — only honoured with NO tools
|
|
59
|
+
tags?: string[],
|
|
60
|
+
|
|
61
|
+
sessionScope?: 'user' | 'org', // Who owns this agent's threads. Default 'user'
|
|
62
|
+
auth?: boolean, // Default false — see below
|
|
63
|
+
scopes?: ScopeId[], // AND gate, checked before permissions
|
|
64
|
+
permissions?: PermissionGroup,
|
|
65
|
+
|
|
66
|
+
middleware?: PikkuMiddleware[],
|
|
67
|
+
channelMiddleware?: PikkuChannelMiddleware[],
|
|
68
|
+
agentMiddleware?: PikkuAgentMiddlewareHooks[],
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**`goal` is the required prompt field, not `instructions`** — there is no
|
|
73
|
+
`instructions` key. `role`/`personality`/`goal` are concatenated in that order,
|
|
74
|
+
and nothing validates which text lands in which, so the split is purely for
|
|
75
|
+
legibility: prose in the "wrong" one still reaches the model.
|
|
76
|
+
|
|
77
|
+
**Tools are `ref('domain:funcName')` handles, not imported function values.** The
|
|
78
|
+
inspector resolves the ref against the generated function map, which is what lets
|
|
79
|
+
an agent reach a function in another package (or a `graph:*` builtin) without an
|
|
80
|
+
import cycle.
|
|
81
|
+
|
|
82
|
+
`auth` defaults to `false` because agents are usually invoked from an
|
|
83
|
+
already-authenticated `pikkuFunc`. `scopes` and `permissions` are enforced either
|
|
84
|
+
way — see `pikku-auth`.
|
|
85
|
+
|
|
86
|
+
### Invoking an agent
|
|
87
|
+
|
|
88
|
+
From inside a Pikku function, go through `wire.rpc.agent` — it carries the
|
|
89
|
+
session, credentials, and RPC depth for you:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const result = await rpc.agent.run('todo-agent', {
|
|
93
|
+
message, threadId, resourceId, // required
|
|
94
|
+
attachments?, model?, temperature?, context?,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
await rpc.agent.stream('todo-agent', input) // writes to the wire's channel
|
|
98
|
+
await rpc.agent.approve(runId, [{ toolCallId, approved }], expectedAgentName?)
|
|
99
|
+
await rpc.agent.resume(runId, { toolCallId, approved })
|
|
100
|
+
await rpc.agent.interrupt(runId, 'user' | 'speech' | 'timeout')
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`context` is a string injected into the system prompt for this request only —
|
|
104
|
+
use it for upfront state (current org, project, deployment) so the agent stops
|
|
105
|
+
asking the user for identifiers it could have been handed.
|
|
106
|
+
|
|
107
|
+
`run` resolves to:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
{
|
|
111
|
+
runId, threadId, text,
|
|
112
|
+
object?, // set when the agent has an `output` schema
|
|
113
|
+
steps, // tool calls made
|
|
114
|
+
usage: { inputTokens, outputTokens },
|
|
115
|
+
status?: 'completed' | 'suspended',
|
|
116
|
+
pendingApprovals?: [{ toolCallId, toolName, args, reason?, runId }],
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`runAgent` / `streamAgent` from `@pikku/core/agent` are the layer beneath
|
|
121
|
+
this. Their third argument is `RunAgentParams` (`{ sessionService?,
|
|
122
|
+
getCredential?, anonymousOwnerResourceId? }`) — **not** `{ singletonServices }`.
|
|
123
|
+
Reach for them only outside a wired function; inside one, `rpc.agent` is the
|
|
124
|
+
supported path.
|
|
125
|
+
|
|
126
|
+
### Stream events
|
|
127
|
+
|
|
128
|
+
`rpc.agent.stream` pushes `AgentStreamEvent`s onto the channel:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// { type: 'step-start', stepNumber }
|
|
132
|
+
// { type: 'text-delta' | 'reasoning-delta', text }
|
|
133
|
+
// { type: 'tool-call', toolCallId, toolName, args }
|
|
134
|
+
// { type: 'tool-result', toolCallId, toolName, result }
|
|
135
|
+
// { type: 'agent-call' | 'agent-result', agentName, session, input | result }
|
|
136
|
+
// { type: 'approval-request', toolCallId, toolName, args, reason?, runId? }
|
|
137
|
+
// { type: 'credential-request', toolCallId, toolName, credentialName,
|
|
138
|
+
// credentialType: 'oauth2' | 'apikey', connectUrl?, runId }
|
|
139
|
+
// { type: 'usage', tokens: { input, output }, model }
|
|
140
|
+
// { type: 'transcript', text } // what the user was heard to say
|
|
141
|
+
// { type: 'audio-delta', data, format, text? } | { type: 'audio-done' }
|
|
142
|
+
// { type: 'data', name, data } | { type: 'generative-ui', spec }
|
|
143
|
+
// { type: 'suspended', reason: 'rpc-missing', missingRpcs }
|
|
144
|
+
// { type: 'interrupted', runId, text, reason }
|
|
145
|
+
// { type: 'error', message }
|
|
146
|
+
// { type: 'done' }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Every event except `agent-call`/`agent-result`/`suspended` also carries optional
|
|
150
|
+
`agent` and `session` fields, so a UI can attribute output to a sub-agent rather
|
|
151
|
+
than folding it into the parent's transcript.
|
|
152
|
+
|
|
153
|
+
## Usage Patterns
|
|
154
|
+
|
|
155
|
+
### Define an Agent
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { pikkuAgent } from '#pikku/agent/pikku-agent-types.gen.js'
|
|
159
|
+
import { ref } from '#pikku/function'
|
|
160
|
+
|
|
161
|
+
export const todoAgent = pikkuAgent({
|
|
162
|
+
name: 'todo-agent',
|
|
163
|
+
description: 'Manages a todo list',
|
|
164
|
+
goal: 'You help users manage their todos. You can list, add, complete and delete them.',
|
|
165
|
+
model: 'openai/gpt-5-mini',
|
|
166
|
+
tools: [
|
|
167
|
+
ref('todos:listTodos'),
|
|
168
|
+
ref('todos:addTodo'),
|
|
169
|
+
ref('todos:completeTodo'),
|
|
170
|
+
ref('graph:sleep'),
|
|
171
|
+
],
|
|
172
|
+
memory: { storage: 'agentStorage', lastMessages: 20 },
|
|
173
|
+
maxSteps: 10,
|
|
174
|
+
toolChoice: 'auto',
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Scaffold the HTTP surface
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
pikku enable agent
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The next `pikku all` generates `agent.gen.ts` — run/stream/approve/resume
|
|
185
|
+
callers plus thread listing endpoints, with thread ownership already enforced
|
|
186
|
+
against the session. Don't hand-write these routes.
|
|
187
|
+
|
|
188
|
+
### Structured output
|
|
189
|
+
|
|
190
|
+
An `output` schema fills `result.object`, but **only when the agent exposes no
|
|
191
|
+
tools** — with a tool present the runner falls back to free text, silently. If
|
|
192
|
+
you need both, split the classification into its own tool-free agent.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
export const structuredAgent = pikkuAgent({
|
|
196
|
+
name: 'structured-agent',
|
|
197
|
+
description: 'Classifies a message and returns a structured verdict',
|
|
198
|
+
goal: 'You classify the sentiment of the user message.',
|
|
199
|
+
model: 'openai/gpt-5-mini',
|
|
200
|
+
output: z.object({ sentiment: z.string(), score: z.number() }),
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Narrowing tools per step
|
|
205
|
+
|
|
206
|
+
`prepareStep` runs before each step with the live tool array for that step, so
|
|
207
|
+
mutating it in place changes what the model is offered from there on. `stop()`
|
|
208
|
+
ends the loop — called before step 0 the run completes with an empty result
|
|
209
|
+
rather than signalling that it was short-circuited.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
prepareStep: ({ stepNumber, tools }) => {
|
|
213
|
+
if (stepNumber >= 1) tools.length = 0 // withdraw tools after the first step
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Tool approval
|
|
218
|
+
|
|
219
|
+
A tool that should pause for a human sets `approvalRequired: true` (with an
|
|
220
|
+
optional `approvalDescription`) on the _function_, not on the agent. The run then
|
|
221
|
+
resolves with `status: 'suspended'` and `pendingApprovals`, and streaming emits
|
|
222
|
+
`approval-request`. Answer with `rpc.agent.approve(runId, approvals)`.
|
|
223
|
+
|
|
224
|
+
Authorization around tools is two-layer: an agent only sees tools its session can
|
|
225
|
+
reach, and the function's own `permissions` still guard the call when the model
|
|
226
|
+
picks one.
|
|
227
|
+
|
|
228
|
+
### Thread ownership
|
|
229
|
+
|
|
230
|
+
`resourceId` is caller-supplied but never trusted as an owner. The session's
|
|
231
|
+
principal (`userId`, or `orgId` when `sessionScope: 'org'`) is prefixed onto it,
|
|
232
|
+
so a client can sub-partition inside its own boundary and cannot read across one.
|
|
233
|
+
A sessionless run gets an ephemeral anonymous owner instead.
|
|
234
|
+
|
|
235
|
+
## Complete Example
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// functions/todos.functions.ts
|
|
239
|
+
export const listTodos = pikkuSessionlessFunc({
|
|
240
|
+
description: 'List all todo items',
|
|
241
|
+
func: async ({ db }, { status }) => {
|
|
242
|
+
return { todos: await db.listTodos(status) }
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
export const createTodo = pikkuFunc({
|
|
247
|
+
description: 'Create a new todo item',
|
|
248
|
+
func: async ({ db }, { text, priority, dueDate }) => {
|
|
249
|
+
return await db.createTodo({ text, priority, dueDate })
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
export const completeTodo = pikkuFunc({
|
|
254
|
+
description: 'Mark a todo as complete',
|
|
255
|
+
func: async ({ db }, { todoId }) => {
|
|
256
|
+
return await db.completeTodo(todoId)
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
// agents/todo-assistant.agent.ts
|
|
261
|
+
import { pikkuAgent } from '#pikku/agent/pikku-agent-types.gen.js'
|
|
262
|
+
import { ref } from '#pikku/function'
|
|
263
|
+
|
|
264
|
+
export const todoAssistant = pikkuAgent({
|
|
265
|
+
name: 'todo-assistant',
|
|
266
|
+
description: 'A helpful assistant that manages todos',
|
|
267
|
+
role: 'You are an assistant that manages a user’s todo list.',
|
|
268
|
+
personality: 'Concise. One short paragraph unless asked for detail.',
|
|
269
|
+
goal: `Keep the user's todos accurate.
|
|
270
|
+
- When creating todos, infer priority if not specified
|
|
271
|
+
- When listing todos, summarize the results`,
|
|
272
|
+
model: 'openai/gpt-5-mini',
|
|
273
|
+
tools: [
|
|
274
|
+
ref('todos:listTodos'),
|
|
275
|
+
ref('todos:createTodo'),
|
|
276
|
+
ref('todos:completeTodo'),
|
|
277
|
+
],
|
|
278
|
+
memory: { storage: 'agentStorage', lastMessages: 20 },
|
|
279
|
+
maxSteps: 5,
|
|
280
|
+
temperature: 0.7,
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// Wire to HTTP for a chat endpoint — or skip this entirely and run
|
|
284
|
+
// `pikku enable agent`, which scaffolds run/stream/approve/resume for you.
|
|
285
|
+
wireHTTP({
|
|
286
|
+
method: 'post',
|
|
287
|
+
route: '/chat',
|
|
288
|
+
func: pikkuFunc({
|
|
289
|
+
title: 'Chat',
|
|
290
|
+
func: async (_services, { message, threadId }, { session, rpc }) => {
|
|
291
|
+
return await rpc.agent.run('todo-assistant', {
|
|
292
|
+
message,
|
|
293
|
+
threadId,
|
|
294
|
+
resourceId: session.userId,
|
|
295
|
+
})
|
|
296
|
+
},
|
|
297
|
+
}),
|
|
298
|
+
})
|
|
299
|
+
```
|
|
@@ -1,27 +1,5 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pikku-ai-vercel
|
|
3
|
-
description: >-
|
|
4
|
-
Use when setting up AI agent execution with the Vercel AI SDK in a Pikku app. Covers
|
|
5
|
-
VercelAgentRunner for streaming and non-streaming AI agent steps. TRIGGER when: code uses
|
|
6
|
-
VercelAgentRunner, user asks about Vercel AI SDK integration, AI agent runners, or
|
|
7
|
-
@pikku/ai-vercel. DO NOT TRIGGER when: user asks about AI agent wiring (use pikku-agent) or
|
|
8
|
-
voice I/O (use pikku-ai-voice).
|
|
9
|
-
installGroups: [fabric]
|
|
10
|
-
---
|
|
11
|
-
|
|
12
1
|
# Pikku AI Vercel (Agent Runner)
|
|
13
2
|
|
|
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
|
-
`@pikku/ai-vercel` provides an AI agent runner backed by the [Vercel AI SDK](https://sdk.vercel.ai/). Implements `AgentRunnerService` from `@pikku/core`.
|
|
25
3
|
|
|
26
4
|
## Installation
|
|
27
5
|
|
|
@@ -47,7 +25,7 @@ const runner = new VercelAgentRunner(
|
|
|
47
25
|
|
|
48
26
|
- `stream(params: AgentRunnerParams, channel: AgentStreamChannel): Promise<AgentStepResult>` — Stream AI responses with tool calls
|
|
49
27
|
- `run(params: AgentRunnerParams): Promise<AgentStepResult>` — Execute a single AI step (non-streaming)
|
|
50
|
-
- `transcribe({ model, audio, … })` / `generateSpeech({ model, text, voice, … })` — what `voiceInput`/`voiceOutput` call; see `
|
|
28
|
+
- `transcribe({ model, audio, … })` / `generateSpeech({ model, text, voice, … })` — what `voiceInput`/`voiceOutput` call; see `references/voice.md`
|
|
51
29
|
- `generateImage`, `embed`, `embedMany`, `rerank` — the remaining AI SDK surfaces
|
|
52
30
|
- `withApiKey(apiKey)` — returns a **new** runner built from `providerFactory`; returns `this` unchanged when no factory was supplied or the key is blank. This is the per-user-credential path
|
|
53
31
|
|
|
@@ -107,7 +85,7 @@ export const assistant = pikkuAgent({
|
|
|
107
85
|
```
|
|
108
86
|
|
|
109
87
|
There is no `wireAgent` — agents are declared with `pikkuAgent` from the
|
|
110
|
-
generated agent types. See `
|
|
88
|
+
generated agent types. See `references/agents.md` for the full config.
|
|
111
89
|
|
|
112
90
|
### Testing without a real provider
|
|
113
91
|
|
|
@@ -1,26 +1,5 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pikku-ai-voice
|
|
3
|
-
description: >-
|
|
4
|
-
Use when adding voice input (speech-to-text) or voice output (text-to-speech) to AI agents in a
|
|
5
|
-
Pikku app. Covers the voiceInput/voiceOutput AI middleware from @pikku/core/agent, per-script
|
|
6
|
-
voices, and barge-in. TRIGGER when: code uses voiceInput, voiceOutput, or user asks about voice
|
|
7
|
-
agents, speech-to-text, text-to-speech, transcription, or @pikku/ai-voice. DO NOT TRIGGER when:
|
|
8
|
-
user asks about AI agent wiring generally (use pikku-agent) or the runner itself (use
|
|
9
|
-
pikku-ai-vercel).
|
|
10
|
-
installGroups: [fabric]
|
|
11
|
-
---
|
|
12
|
-
|
|
13
1
|
# Pikku AI Voice (Speech I/O)
|
|
14
2
|
|
|
15
|
-
## Agent Operating Procedure
|
|
16
|
-
|
|
17
|
-
Use this skill as an execution checklist, not reference material.
|
|
18
|
-
|
|
19
|
-
1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
|
|
20
|
-
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.
|
|
21
|
-
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
|
|
22
|
-
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.
|
|
23
|
-
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
|
|
24
3
|
|
|
25
4
|
## `@pikku/ai-voice` is deprecated and empty
|
|
26
5
|
|
|
@@ -30,7 +9,7 @@ dependency.
|
|
|
30
9
|
|
|
31
10
|
Voice now lives in **`@pikku/core/agent`** as two AI middlewares, and the
|
|
32
11
|
speech models are reached through the `agentRunner` (`transcribe` /
|
|
33
|
-
`generateSpeech`) rather than through separate services. See `
|
|
12
|
+
`generateSpeech`) rather than through separate services. See `references/runner-vercel.md`.
|
|
34
13
|
|
|
35
14
|
## API Reference
|
|
36
15
|
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pikku-architect
|
|
3
|
+
description: >-
|
|
4
|
+
Use to turn one settled milestone note into the technical plan the build is measured against —
|
|
5
|
+
the tables, functions, wires, roles, scopes, screens and scenarios it owes, split into passes and
|
|
6
|
+
written through `pikku knowledge plan set`. This is a SEPARATE SEAT from the build: the plan is
|
|
7
|
+
the denominator `pikku knowledge plan progress` divides by, so whoever writes it must not be the
|
|
8
|
+
one grading themselves against it. TRIGGER when: a milestone note is settled and the next step is
|
|
9
|
+
planning it, the user asks to plan or architect a milestone, `pikku knowledge plan progress` says
|
|
10
|
+
a milestone has no plan, or pikku-build's App mode reaches a milestone with nothing planned. DO
|
|
11
|
+
NOT TRIGGER when: the milestone notes themselves are still being written (use pikku-knowledge),
|
|
12
|
+
the plan already exists and the job is to build it (use pikku-build), or the ask is a one-off
|
|
13
|
+
edit to a working app.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Plan one milestone
|
|
17
|
+
|
|
18
|
+
A milestone note says what the app must DO and how it must feel for the person using it. It
|
|
19
|
+
deliberately does not say how. You are the seat that decides how, once, in writing, before anyone
|
|
20
|
+
builds it.
|
|
21
|
+
|
|
22
|
+
**Why this is a separate seat.** The build agent used to write its own plan. That makes one party
|
|
23
|
+
both author and examiner: it can build a fraction, plan only that fraction, and certify itself
|
|
24
|
+
complete — and `pikku knowledge plan progress` then divides by a denominator the builder chose
|
|
25
|
+
after seeing its own answer. A plan written here, against the note, by someone who is not going to
|
|
26
|
+
build it, is the denominator the builder does not own.
|
|
27
|
+
|
|
28
|
+
**One milestone, one plan, then stop.** Do not build in this session. Do not plan the next
|
|
29
|
+
milestone "while you are here" — the notes after this one are still allowed to change, and a plan
|
|
30
|
+
written against a note that later moves is worse than no plan.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Write nothing by hand
|
|
35
|
+
|
|
36
|
+
The plan reaches disk through `pikku knowledge plan set <milestone> <file>` and nowhere else. It
|
|
37
|
+
validates first and names the field that is wrong if it refuses; a plan file written with an editor
|
|
38
|
+
is a plan nothing checked, and the place that discovers that is a finished build.
|
|
39
|
+
|
|
40
|
+
It is JSON rather than a note on purpose. Everything else under `knowledge/` is prose a human
|
|
41
|
+
reads; this one is consumed field-by-field, and a markdown parser is one more place a misspelt
|
|
42
|
+
heading silently passes. It cannot live INSIDE the milestone note either: that note is frozen once
|
|
43
|
+
its status leaves `proposed`, so rewriting it would change what the builder was told.
|
|
44
|
+
|
|
45
|
+
## Send it. Do not go looking.
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pikku knowledge plan schema
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That is the specification, in full, with every field's guidance in its `description`. There is no
|
|
52
|
+
second plan-format doc. So when you are unsure what a field wants, **write your best honest reading
|
|
53
|
+
and send it** — `plan set` validates every field and names the exact one that is wrong, so a wrong
|
|
54
|
+
guess costs one round trip and teaches you the answer.
|
|
55
|
+
|
|
56
|
+
The failure mode to recognise in yourself: you have decided the tables, the passes and the
|
|
57
|
+
functions, and you are still reading. That is the moment to run `plan set`.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## The turn
|
|
62
|
+
|
|
63
|
+
### 1. Read what has been settled
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
pikku knowledge validate # the base is consistent before you plan against it
|
|
67
|
+
pikku meta context --json # what the app already declares
|
|
68
|
+
pikku knowledge plan schema # the only spec for what you are about to write
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then read, in the tree: the milestone's own note in full, every note it names on `entities:` and
|
|
72
|
+
`requires:`, the decisions that constrain it, and the migrations already in `db/sqlite/` — those
|
|
73
|
+
say whether your tables are new or an alter.
|
|
74
|
+
|
|
75
|
+
**Do not re-interview.** If the note leaves something genuinely undecided, plan the reading that
|
|
76
|
+
builds LESS. A smaller milestone that ships is worth more than a complete one that does not, and
|
|
77
|
+
what you leave out is named in `covers` for the next milestone to pick up.
|
|
78
|
+
|
|
79
|
+
### 2. Decide the passes
|
|
80
|
+
|
|
81
|
+
A pass is a slice of the milestone that stands up on its own. **Pass 1 is a walking skeleton**: it
|
|
82
|
+
reaches a real screen, with real functions behind it, proved by a real browser scenario. Everything
|
|
83
|
+
else waits behind it.
|
|
84
|
+
|
|
85
|
+
This is enforced, not advisory — `plan set` refuses a plan whose pass 1 has no `ui` item, no
|
|
86
|
+
`functions` item, or a pass-1 route with nothing proving it works. The reason is the failure it was
|
|
87
|
+
written against: a milestone that built four unwired functions and no page, and reported itself
|
|
88
|
+
finished. A build that runs out of time in pass 2 has shipped something; one that runs out of time
|
|
89
|
+
having built pass 1 across four half-finished layers has shipped nothing.
|
|
90
|
+
|
|
91
|
+
**Only pass 1 blocks.** `pikku knowledge plan progress` reports a later pass under `deferred` and
|
|
92
|
+
never refuses on it. That is what stops plan size from being fatal — but it is not licence to plan
|
|
93
|
+
a milestone nobody could finish. The question that decides a plan's size is not "what does this
|
|
94
|
+
note imply" but **"could a build finish all of this if pass 1 took twice as long as I expect"** — if
|
|
95
|
+
not, it is two milestones. Plan the first, and say in `covers` what you left behind.
|
|
96
|
+
|
|
97
|
+
**A screen is what pass 1 reaches only when the milestone IS an app.** The note's `surface:` says
|
|
98
|
+
which it is — absent means an app, and `cli`, `mcp`, `agent` and `backend` are the others. On those,
|
|
99
|
+
`ui` is legitimately `n/a` (with its reason, like any slot), and pass 1 proves itself one level
|
|
100
|
+
down: a pass-1 function that is actually wired, and a `scenarios.backend` item carrying that
|
|
101
|
+
function's name in its `fn` field. The obligation never lifts, it only moves — read the surface off
|
|
102
|
+
the note before you decide the passes.
|
|
103
|
+
|
|
104
|
+
### 3. Say what each slot is, or say why it is nothing
|
|
105
|
+
|
|
106
|
+
Every slot — `model`, `functions`, `roles`, `scopes`, `ui`, and each level of `scenarios` — is
|
|
107
|
+
either `{"kind": "built", "description": ..., "items": [...]}` or `{"kind": "n/a", "description":
|
|
108
|
+
...}`. **Both carry prose.**
|
|
109
|
+
|
|
110
|
+
There is no way to leave a slot out, and that is the point: "no roles, because everyone using this
|
|
111
|
+
app is the same kind of person" and "nobody thought about roles" must not look alike. Write the
|
|
112
|
+
`n/a` reason as a sentence a reader would accept, not as the word "none".
|
|
113
|
+
|
|
114
|
+
### 4. Write it
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
pikku knowledge plan set <milestone> /tmp/plan.json
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Write the JSON to a file first — the command takes a path, not inline JSON, which is what keeps an
|
|
121
|
+
apostrophe in a `description` from ending a shell argument. If it is refused, the refusal names the
|
|
122
|
+
field path. Fix that field and send it again; do not restructure the plan around a refusal you have
|
|
123
|
+
not read.
|
|
124
|
+
|
|
125
|
+
Then confirm what the builder will be handed:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
pikku knowledge plan show <milestone> --for-build
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## What the plan holds
|
|
134
|
+
|
|
135
|
+
The plan holds INTENT. Reality lives in pikku's generated meta under `.pikku/`, which already
|
|
136
|
+
inventories every function, wire, scope, role, workflow, agent and scenario. Nothing here
|
|
137
|
+
duplicates that — only what codegen cannot infer: **why a thing exists, which pass it belongs to,
|
|
138
|
+
and which knowledge note it discharges.**
|
|
139
|
+
|
|
140
|
+
### `covers` — which notes this milestone discharges
|
|
141
|
+
|
|
142
|
+
Every plan claims at least one knowledge note: `note` (its path under `knowledge/`), `hash` (what
|
|
143
|
+
that note's body hashes to right now) and `complete`.
|
|
144
|
+
|
|
145
|
+
`complete: false` is the honest answer for a note whose claims span several milestones — claim the
|
|
146
|
+
whole of a note only when this milestone genuinely leaves nothing of it unbuilt, because a note
|
|
147
|
+
marked complete is a note nobody looks at again.
|
|
148
|
+
|
|
149
|
+
**You do not have to compute the hash.** Write anything twelve characters long and send the plan:
|
|
150
|
+
`plan set` refuses a hash that is not the note's current one and names the correct one, so one
|
|
151
|
+
round trip gets you every hash in the plan. That refusal is the point of the field — a hash that
|
|
152
|
+
was never right makes the note read as edited-since from the moment the milestone ships, and it
|
|
153
|
+
drops back into a backlog nobody planned.
|
|
154
|
+
|
|
155
|
+
### `model` — tables, and what their columns HOLD
|
|
156
|
+
|
|
157
|
+
Each field carries a `classification`: `public`, `internal`, `personal` or `sensitive`. That is what
|
|
158
|
+
lets a permission claim be checked against the data rather than only against itself — a function
|
|
159
|
+
returning a `personal` column with no permission rule is a defect the gate can name. It is also what
|
|
160
|
+
`db/annotations.ts` ends up expressing, so plan it here rather than discovering it at migrate time.
|
|
161
|
+
|
|
162
|
+
Each relationship carries `onDelete`: `cascade`, `restrict` or `orphan`. A foreign key states which
|
|
163
|
+
rows are related; it does not state what the product wants when the parent goes, and those three
|
|
164
|
+
produce identical schemas until someone deletes something. A `cascade` is checked against the
|
|
165
|
+
migrations by `plan progress`, and needs `provedBy` naming a scenario in this same plan that deletes
|
|
166
|
+
the parent and asserts the children are gone.
|
|
167
|
+
|
|
168
|
+
A table that already exists is altered by a NEW forward migration, numbered on from the ones in
|
|
169
|
+
`db/sqlite/`. Editing an applied migration is the hash mismatch that makes a deployed database
|
|
170
|
+
refuse to migrate, so plan the alter as its own file.
|
|
171
|
+
|
|
172
|
+
### `functions` — with their wire and their rule on them
|
|
173
|
+
|
|
174
|
+
The wire and the permission live ON the function, because that is where pikku enforces them. Two
|
|
175
|
+
parallel lists are two lists that drift.
|
|
176
|
+
|
|
177
|
+
**Do not give a function a `wire`.** pikku already serves every `expose: true` function as an RPC
|
|
178
|
+
and the client calls it by name, so for nearly every function there is nothing to decide — leave the
|
|
179
|
+
field out. A `wire` is for the exceptions: its own HTTP path via `wireHTTP` (a webhook, a payment
|
|
180
|
+
callback, a public URL another system posts to), a queue job, a channel, a scheduled task, or a
|
|
181
|
+
workflow entry point. Those last two are not alternate URLs — they are what the milestone IS, and a
|
|
182
|
+
plan that omits them ships a `status` column nothing advances or a job nobody runs.
|
|
183
|
+
|
|
184
|
+
`permission` is a SENTENCE, not a role name — "only the person who wrote it can edit it". The roles
|
|
185
|
+
are the engineer's choice; the rule is the part that has to survive being implemented, in the
|
|
186
|
+
function's `permissions` field and never in its body. `null` means open to anyone signed in, and
|
|
187
|
+
stating that is different from omitting it. **Every function with a permission rule needs a
|
|
188
|
+
permission scenario naming it in `fn`** — a rule with no failing case is a claim, not a check, and
|
|
189
|
+
`plan set` refuses the plan without one.
|
|
190
|
+
|
|
191
|
+
### `scopes` — what a KIND of user may do, never who owns a row
|
|
192
|
+
|
|
193
|
+
A scope depends ONLY on the session: "may this kind of user do this at all" — `admin:invoices:void`,
|
|
194
|
+
`billing`. It is declared with `wireScope` and granted in `mapSession`, so every name here has to
|
|
195
|
+
end up in pikku's generated scope meta. One that cannot be declared is one the build can never
|
|
196
|
+
finish, and `plan progress` refuses the milestone for as long as it stands.
|
|
197
|
+
|
|
198
|
+
Ownership is not a scope. "Only the owner of the house may read it" depends on the row being asked
|
|
199
|
+
for, and a scope never sees the row — that is the function's `permission` sentence and lives nowhere
|
|
200
|
+
else. If the rule mentions the record, it is a `permission`; if it reads the same for every row that
|
|
201
|
+
user touches, it is a scope. An app built from one person's idea usually has none at all, so
|
|
202
|
+
`{"kind": "n/a", ...}` is the ordinary answer here.
|
|
203
|
+
|
|
204
|
+
### `roles` — and the app each one signs into
|
|
205
|
+
|
|
206
|
+
The distinct `app` values across `roles` ARE the frontends this project gets, and nothing downstream
|
|
207
|
+
can recover the answer. Colleagues share ONE app and differ by nav and permitted actions (the
|
|
208
|
+
mechanic, the person on the counter, the bookkeeper); someone across the counter with an account
|
|
209
|
+
gets their own (the customer, the tenant, the patient). One app is a real answer and often the right
|
|
210
|
+
one — then every role carries the same slug. Never invent a person the notes do not name in order to
|
|
211
|
+
reach two, and never give a slug to someone who never signs in: a guest checking out takes the same
|
|
212
|
+
slug as the seller they buy from, on that app's public routes outside `/app`. Once there is more
|
|
213
|
+
than one app, every `ui` item carries its `app` too.
|
|
214
|
+
|
|
215
|
+
Adding the second frontend is the BUILD's job, at the milestone that first needs it —
|
|
216
|
+
pikku-build's multi-app reference. Your part is recording which app each person is in.
|
|
217
|
+
|
|
218
|
+
### `ui` — routes, and what is on them
|
|
219
|
+
|
|
220
|
+
One item per route, each with the pass that builds it. A pass-1 route has to be LINKED to the
|
|
221
|
+
scenario that proves it, and there are two ways: name the scenario in that `ui` item's own
|
|
222
|
+
`scenarios` array, or write a browser scenario whose `feature` contains the route path. Nothing else
|
|
223
|
+
counts — an unlinked browser scenario reads as a route nobody proved, and the plan is refused.
|
|
224
|
+
|
|
225
|
+
### `scenarios` — keyed by level
|
|
226
|
+
|
|
227
|
+
`backend`, `browser`, `permission`, each its own slot. Keyed rather than tagged so that a plan with
|
|
228
|
+
four backend scenarios and no browser scenario fails on its SHAPE — a flat list lets that through,
|
|
229
|
+
and that is exactly the milestone that builds an API and ships no screen.
|
|
230
|
+
|
|
231
|
+
**Every scenario needs `name`: the `pikkuScenario` export it becomes** (`saveEntryScenario`).
|
|
232
|
+
`feature` and `scenario` are prose for a reader, and prose cannot be matched against codegen.
|
|
233
|
+
`plan progress` looks for the export by that exact name, so a scenario with no name is one the gate
|
|
234
|
+
cannot see.
|
|
235
|
+
|
|
236
|
+
Permission scenarios default to pass 2 — they harden a journey that has to exist before they can
|
|
237
|
+
cover it — so a role × resource cross product there costs the milestone nothing.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## What makes a plan wrong
|
|
242
|
+
|
|
243
|
+
`plan set` catches the mechanical failures. These are the ones it cannot:
|
|
244
|
+
|
|
245
|
+
- **A plan for a different milestone.** The note is about `entries`; the plan builds `projects`.
|
|
246
|
+
Every entity the note names must appear in a function or a table.
|
|
247
|
+
- **A pass 1 that is a layer, not a slice.** "Pass 1: the data model. Pass 2: the API. Pass 3: the
|
|
248
|
+
screens." That is three passes of nothing working.
|
|
249
|
+
- **Scenarios that assert the code ran rather than that the person got what they came for.** A
|
|
250
|
+
scenario proving `saveEntry` returns 200 proves the wire. The one worth planning is the one where
|
|
251
|
+
a person writes something, comes back, and it is still there. A browser scenario that opens a page
|
|
252
|
+
and asserts it is still on it proves the route loads and nothing else —
|
|
253
|
+
`pikku knowledge plan progress` names it as a problem and refuses the milestone.
|
|
254
|
+
- **A permission rule invented here.** If the notes do not say who may do a thing, the answer is
|
|
255
|
+
`null` with the reason, not a rule you made up. A rule the user never agreed to is one they find
|
|
256
|
+
out about by being locked out of their own app.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## When you are done
|
|
261
|
+
|
|
262
|
+
The accepted `plan set` is the end of the seat. Hand the milestone to `pikku-build`, which reads the
|
|
263
|
+
plan with `plan show --for-build`, builds it, and closes the milestone only when
|
|
264
|
+
`pikku knowledge plan progress` is clean. What you wrote is what it is measured against.
|