@pikku/skills 0.12.49 → 0.12.51

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.
@@ -4,7 +4,7 @@ A scenario is a user journey run as one of your personas, over the real transpor
4
4
  persona's session. It is the only kind of test worth writing here, because a passing one proves the
5
5
  app works the way a signed-in person experiences it.
6
6
 
7
- The traps below all cost a real milestone a red run, and most of them are invisible on the run that
7
+ The traps below all cost a real changeset a red run, and most of them are invisible on the run that
8
8
  introduces them. Read the section that matches what you are writing.
9
9
 
10
10
  1. [The shape of a scenario](#the-shape-of-a-scenario)
@@ -20,8 +20,8 @@ introduces them. Read the section that matches what you are writing.
20
20
 
21
21
  ## The shape of a scenario
22
22
 
23
- Three ship in `packages/functions/test/scenarios/` — keep them green — and every milestone's gherkin
24
- block from §5 becomes one more.
23
+ Three ship in `packages/functions/test/scenarios/` — keep them green — and every scenario a
24
+ changeset's plan names becomes one more.
25
25
 
26
26
  ```typescript
27
27
  import { pikkuScenario } from '#pikku/scenarios'
@@ -83,7 +83,7 @@ how the value stays inspectable when the step's output is what the scenario retu
83
83
  **Only `const`/`let`, `if`/`else`, `switch`, `for..of`, `return`, `throw` and workflow calls
84
84
  survive.** A counting `for` is refused by PKU679, and so is a `for..of` whose iterable is written
85
85
  inline — it must be a named identifier or a field (`data.items`). Bind the seat numbers, the ids,
86
- the rows to a `const` above the loop and iterate that. One milestone here lost a codegen round to
86
+ the rows to a `const` above the loop and iterate that. One changeset here lost a codegen round to
87
87
  each half of that rule, because the first half does not imply the second.
88
88
 
89
89
  **Write each scenario's setup out in full rather than sharing a local helper.** A helper holding
@@ -137,7 +137,7 @@ one that archives a product unarchives it, the one that cancels a plan restarts
137
137
  second run starts where its first one stopped.
138
138
 
139
139
  **Run the suite twice and require the second run green.** A suite that only passes on a fresh
140
- database is a suite that passes once. Two corollaries, both of which cost a milestone a red run:
140
+ database is a suite that passes once. Two corollaries, both of which cost a changeset a red run:
141
141
 
142
142
  - **Name nothing a setup step might already own.** `setsUpHerCompany` returns the company that actor
143
143
  already has rather than renaming it, so a later step passed the literal name it had asked for and
@@ -160,11 +160,11 @@ when a sweep surprises you, the next rule in the calendar is the first place to
160
160
 
161
161
  ## Steps rot as the app grows
162
162
 
163
- A step is shared, so it is the one thing in the suite that a milestone which never mentions it can
163
+ A step is shared, so it is the one thing in the suite that a changeset which never mentions it can
164
164
  break. Whatever a step selects by, ask what ELSE will match it after the suite has run a hundred
165
165
  times.
166
166
 
167
- **Drive a new step from both sides in the milestone that adds it.** A step is only as proven as its
167
+ **Drive a new step from both sides in the changeset that adds it.** A step is only as proven as its
168
168
  best-exercised branch: one here read the wrong field off a raw invocation (`attempt.data`; the
169
169
  payload is `attempt.body`), so its found-case could never pass — invisible for as long as every
170
170
  caller asked for ABSENCE.
@@ -175,9 +175,9 @@ moment a second function produces X: a renewal job that raised invoices broke th
175
175
  scenarios that had been green for months.
176
176
 
177
177
  **A selector on IDENTITY alone rots the same way once rows gain a lifecycle.** One reused the first
178
- licence assigned to an email regardless of its state, which was correct until a new milestone's
178
+ licence assigned to an email regardless of its state, which was correct until a new changeset's
179
179
  refusal scenarios left that actor holding cancelled ones — it then handed back a dead licence, read
180
- as success, and failed a scenario two milestones older at a step that needed a live one.
180
+ as success, and failed a scenario two changesets older at a step that needed a live one.
181
181
 
182
182
  **A step's input is a recorded contract, and it does not take a version.** Widening one — an extra
183
183
  optional field so a step can name a particular row — trips PKU861 exactly like a function's does,
@@ -218,10 +218,10 @@ Both are screen defects before they are test defects: a field whose label change
218
218
  and a list whose rows are indistinguishable to anyone on the phone to support.
219
219
 
220
220
  **A route nested under an existing screen is unreachable until its parent renders an `Outlet`.** A
221
- milestone added `/app/academy/$slug` under an `/app/academy` that already had a component of its
221
+ changeset added `/app/academy/$slug` under an `/app/academy` that already had a component of its
222
222
  own; the parent swallowed the child, so the editor's URL rendered the list — every link, every route
223
223
  file and every type check looked right, and the only thing that noticed was a browser scenario that
224
- OPENED the child path and found the parent's controls on screen. When a milestone deepens a path an
224
+ OPENED the child path and found the parent's controls on screen. When a changeset deepens a path an
225
225
  earlier one already owns, split the parent into a layout (`Outlet`) and an `index` route in the same
226
226
  change, and make one scenario open the child by path rather than reach it by clicking.
227
227
 
@@ -248,9 +248,9 @@ environment's `appUrls` map, so there is no second environment to run.
248
248
  browser pass needs the environment's `appUrl` and a browser driver installed — without them the run
249
249
  fails fast rather than half-running.
250
250
 
251
- **Run the whole suite, not the milestone's own scenarios.** The milestone's scenarios are the ones
251
+ **Run the whole suite, not the changeset's own scenarios.** The changeset's scenarios are the ones
252
252
  you wrote to pass; the regression lives in someone else's. Tightening what "archived" means is a
253
- one-function change that reads as local and quietly breaks the milestone-01 scenario nobody re-ran.
253
+ one-function change that reads as local and quietly breaks the first changeset's scenario nobody re-ran.
254
254
 
255
255
  **Restart the server after adding a function.** Hot reload does not register a new RPC and does not
256
256
  re-run `afterStart`, so a fresh function answers 404 and anything provisioned at boot is missing —
@@ -272,9 +272,9 @@ That writes `coverage/scenario-coverage.json` — which functions each journey e
272
272
  no scenario touches has never been run by anything but you, by hand, once.** It compiles, it
273
273
  typechecks, `pikku all` is happy, and nobody has proven it does what it says.
274
274
 
275
- Run it **as each milestone closes**, not once at the end. Coverage read per milestone is a short
276
- list you can act on — the milestone you just built either covered its own functions or it did not.
277
- Read for the first time after ten milestones it is a wall of red that nobody triages, and the honest
275
+ Run it **as each changeset closes**, not once at the end. Coverage read per changeset is a short
276
+ list you can act on — the changeset you just built either covered its own functions or it did not.
277
+ Read for the first time after ten changesets it is a wall of red that nobody triages, and the honest
278
278
  response to a wall of red is to ignore it.
279
279
 
280
280
  Every gap is one of three things, and naming which is the point of looking:
@@ -285,8 +285,8 @@ Every gap is one of three things, and naming which is the point of looking:
285
285
  - **A function that should not exist** — nothing reaches it because nothing needs it. Delete it. An
286
286
  unused exposed function is also reachable over `POST /rpc/:rpcName`, so this is a security finding,
287
287
  not only dead weight.
288
- - **Genuinely deferred** — real, not yet reachable from the UI. Say so in the milestone note that
289
- will cover it, so the gap is a decision rather than a hole.
288
+ - **Genuinely deferred** — real, not yet reachable from the UI. File it as a change, so the gap is
289
+ a decision rather than a hole.
290
290
 
291
- Report the number when you hand the milestone over. A number nobody says out loud is a number nobody
291
+ Report the number when you hand the changeset over. A number nobody says out loud is a number nobody
292
292
  acts on.
@@ -1,6 +1,6 @@
1
1
  # Shipping, and staying Fabric-ready
2
2
 
3
- Read this when the milestones are built and the scenarios are green — it is
3
+ Read this when the changes queue is empty and the scenarios are green — it is
4
4
  the last phase, and nothing in it is needed before then.
5
5
 
6
6
  ## Ship it — open source, no platform
@@ -128,8 +128,8 @@ bun run build # every frontend workspace, type-checked
128
128
  ```
129
129
 
130
130
  Keep `--coverage` on the release run even though you have been reading it per
131
- milestone (§7a). Each of those readings only covered the functions that
132
- milestone added; this is the first time the whole surface is measured at once,
131
+ changeset (§7a). Each of those readings only covered the functions that
132
+ changeset added; this is the first time the whole surface is measured at once,
133
133
  and it is where a function orphaned by a later refactor shows up.
134
134
 
135
135
  **The last two lines are not optional, and one of them is easy to talk yourself
@@ -168,15 +168,15 @@ Everything above is open source. This is the contract that keeps
168
168
  - **One `definePersonas` call**, every persona reachable through exactly one
169
169
  frontend. Fabric materialises these as its virtual users; a persona nobody
170
170
  serves imports as a person with no way in.
171
- - **`knowledge/` passes `validate`, with every milestone at `built`.** This is
171
+ - **`knowledge/` passes `validate`, and `pikku knowledge gaps` lists nothing.** This is
172
172
  the part Fabric itself reads and continues from.
173
- - **Every `built` milestone passes `pikku knowledge plan progress`.** A note that
174
- says `built` is a claim; the plan reconciled against the generated meta is the
175
- check. Anything the first pass still owes is either built now or deferred with
173
+ - **Every plan under `knowledge/plans/` passes `pikku knowledge plan progress`.**
174
+ A merged changeset is a claim; the plan reconciled against the generated meta
175
+ is the check. Anything the first pass still owes is either built now or deferred with
176
176
  its reason on the record; anything the check calls a problem — something that
177
177
  exists and does not do what was planned — is fixed, whatever pass it came from,
178
178
  because deferring it defers a hole rather than the work.
179
- - **Every milestone has a passing scenario**, including its refusals.
179
+ - **Every changeset has a passing scenario**, including its refusals.
180
180
  - **Permissions live in the `permissions` field**, not in function bodies and not
181
181
  in the frontends. A check hidden in a component does not survive a new client.
182
182
  - **Nothing hardcodes a host, a port, or a `process.env` read inside a
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pikku-changes
3
- description: 'Work a Fabric project''s changes queue — the todo list someone filed by circling things on a deployed stage. Covers `pikku fabric changes next|claim|show|ask|reply|shot|done`: waiting for work without polling, asking instead of guessing, saying why an item is left undone, offering options as images, one commit per item. TRIGGER when: the user says "run the pikkufabric changes", "run the changes against <stage>", "work the changes (queue)", "watch the changes", "pick up the changes", names a change by its #number, or you are otherwise idle in a checkout linked to a Fabric project (`pikku fabric config` shows one). DO NOT TRIGGER for git changes, diffs or changelogs, and not for deploying or debugging a stage — use pikku-fabric for those.'
3
+ description: 'Work a project''s changes queue — the todo list someone filed by circling things on a deployed stage. Covers `pikku changes wait|claim|show|ask|reply|shot|done`: waiting for work without polling, asking instead of guessing, saying why an item is left undone, offering options as images, one commit per item. TRIGGER when: the user says "run the pikkufabric changes", "run the changes against <stage>", "work the changes (queue)", "watch the changes", "pick up the changes", names a change by its #number, or you are otherwise idle in a checkout with open changes (`pikku changes list`). DO NOT TRIGGER for git changes, diffs or changelogs, and not for deploying or debugging a stage — use pikku-fabric for those.'
4
4
  installGroups: [fabric]
5
5
  ---
6
6
 
@@ -10,60 +10,107 @@ Someone walked the deployed app and circled things. Each item is their words, a
10
10
  screenshot of what they saw, and the elements the circle enclosed. You have the repo.
11
11
  Empty the queue without making them regret filing.
12
12
 
13
- Run every command from the checkout: the project comes from its git remote (`pikku fabric config` shows which).
13
+ Run every command from the checkout. The queue lives in it (`.git/pikku-changes.json`, shared by every
14
+ worktree). When you are logged in to Fabric and the checkout is linked to a project (`pikku fabric config`
15
+ shows which), every write is also registered with Fabric.
14
16
  `--json` works on all of them. Items are addressed as `2`, `#2` or their uuid.
15
17
 
16
- ## Which stage
17
-
18
- The queue is per project. "Against develop" or a pasted stage URL narrows it:
19
- `--stage` takes a branch, the stage URL (as filed, path optional) or a stage id. With
20
- no stage named, work the whole project. An unknown name prints the stages there are.
18
+ **Launched by `pikku changes next`?** `pikku changes next` picks one agent: a merge conflict or a changeset with no plan
19
+ goes back to a changes agent; open changes go to a changes agent; a pikku version bump goes to an
20
+ upgrade agent, and knowledge no change builds yet (`pikku knowledge gaps`) to a knowledge agent — both
21
+ of those only file changes. A change filed for a gap ends its body with the gap's `Knowledge:` line.
22
+ As a changes agent, your work file already lists every open change. Skip the loop below,
23
+ group them into changesets, and take one: claim it, build it, mark its items done, stop. `pikku changes next --loop`
24
+ starts a fresh agent for the next one, so nothing you hold in context carries over — whatever the next
25
+ changeset needs to know goes in a commit or a `reply`.
21
26
 
22
27
  ## The loop
23
28
 
24
29
  **Never poll.** No `sleep` loops, no repeated `list`, no re-running `show` to see if
25
- something changed. `next` does the waiting and exits only when there is work.
30
+ something changed. `wait` does the waiting and exits only when there is work.
26
31
 
27
- 1. Start `next` as a **background** command, and stop there until it exits:
32
+ 1. Start `wait` as a **background** command, and stop there until it exits:
28
33
 
29
34
  ```bash
30
- pikku fabric changes next --stage develop --claim --claimed-by claude-code
35
+ pikku changes wait --claim --claimed-by claude-code
31
36
  ```
32
37
 
33
38
  It waits out the grace window (a just-filed item is held about a minute so a batch
34
39
  being typed arrives together), claims what is ready as one group, prints it, and
35
40
  exits. It also wakes when someone answers a question you asked under that
36
- `--claimed-by`. It is woken by fabric's change events the moment an item is filed
37
- or answered, and sleeps exactly until a held item becomes claimable; when the event
38
- stream is unavailable it falls back to checking every `--interval` seconds.
41
+ `--claimed-by`. It checks every `--interval` seconds.
39
42
 
40
43
  2. When it exits, read the exit code:
41
44
 
42
45
  | code | meaning | do |
43
46
  | ---- | ------------------------------------------------------ | ----------------------------------------------- |
44
47
  | 0 | work printed (claimed, and/or `Answered`) | work it, then step 3 |
45
- | 2 | `--timeout`/`--once` found nothing | stop, or restart `next` |
48
+ | 2 | `--timeout`/`--once` found nothing | stop, or restart `wait` |
46
49
  | 3 | session refused | tell the user to run `pikku fabric login`; stop |
47
- | 1 | anything else (bad `--stage`, fabric down for minutes) | report the message; stop |
50
+ | 1 | anything else | report the message; stop |
48
51
 
49
52
  3. For each item: `show` → fix → commit → `done`; or `ask` and move on; or `reply`
50
- saying why you are leaving it. Then start `next` again, in the background.
53
+ saying why you are leaving it. Then start `wait` again, in the background.
51
54
 
52
55
  Without `--claim` it only reports what is claimable; claim it yourself:
53
56
 
54
57
  ```bash
55
- pikku fabric changes claim --change-ids 3,4 --title "Checkout pass" --claimed-by claude-code
58
+ pikku changes claim --change-ids 3,4 --title "Checkout pass" --claimed-by claude-code
56
59
  ```
57
60
 
58
61
  A `claim` refused with a 409 says per item why — held for the filer, inside another
59
62
  group's lease, done — and when a held or leased item is **claimable at** (a local
60
63
  `HH:MM`; `list` and `show` print the same). An item inside someone else's live lease
61
- cannot be taken. For held items, run `next --claim` rather than retrying. The lease is 30 minutes
64
+ cannot be taken. For held items, run `wait --claim` rather than retrying. The lease is 30 minutes
62
65
  (`--lease-minutes`); an abandoned claim returns to the queue by itself.
63
66
 
67
+ ## Changesets
68
+
69
+ Group related items into changesets and claim each one as a group, saying which tables it touches —
70
+ the entity notes' `resource:` lines name them:
71
+
72
+ ```bash
73
+ pikku changes claim --change-ids 1,3 --title "Waitlist" --claimed-by pi --creates waitlist --reads booking
74
+ ```
75
+
76
+ The claim says whether the changeset needs a plan, and why: one that creates or alters a table, or
77
+ has many changes, always does; anything else goes to the judge configured with
78
+ `PIKKU_PLAN_JUDGE_URL` (any endpoint answering `{verdict, reason}` to `{question, context}`), and a
79
+ judge that fails says plan. With no judge configured only the fixed rules apply. `--needs-plan
80
+ true|false` overrides all of it.
81
+
82
+ A planned changeset is planned before any code, with the pikku-architect skill:
83
+ `pikku knowledge plan set <groupId> <file>` writes `knowledge/plans/<groupId>.plan.json`; commit it on
84
+ the changeset's branch. `done` refuses the first change until the plan reads and the last until
85
+ `pikku knowledge plan progress <groupId>` is clean, and `pikku changes next` will not merge a planned
86
+ changeset whose branch has no plan.
87
+
88
+ Build each changeset on its own branch, `changeset/<slug>`, cut from the branch you started on, one
89
+ commit per item (see Committing), and mark each item `done`. Launched by `pikku changes next`, stop there:
90
+ it merges finished changesets itself, as one `--no-ff` commit with a `Changeset:` trailer, and if the
91
+ merge conflicts it hands that back to an agent to resolve on the changeset's branch. Working by hand,
92
+ merge it yourself from the branch it goes into:
93
+
94
+ ```bash
95
+ pikku changes merge --group-id <id>
96
+ ```
97
+
98
+ Never `git merge` a changeset branch yourself — a fast-forward leaves no changeset commit, and
99
+ `changes merge` refuses a branch that is already in.
100
+
101
+ Changesets that create or alter tables go one at a time; one that reads a table waits for the
102
+ changeset creating it; the rest can run side by side. On the local queue, `claim` refuses a changeset
103
+ that would break that order, and `done` refuses a commit that adds a migration under `db/` when its
104
+ changeset declared no `--creates`/`--alters`.
105
+
106
+ When other agents are working changesets at the same time (your work says so), claim with
107
+ `--worktree`: it creates `changeset/<slug>` in its own checkout beside the repo and prints the path.
108
+ Build and commit there and run `done` there; the merge removes the worktree. If the claim is refused because of a running changeset, claim one that does not
109
+ clash, or stop.
110
+
64
111
  ## Reading an item
65
112
 
66
- `pikku fabric changes show 3` gives, most trustworthy first:
113
+ `pikku changes show 3` gives, most trustworthy first:
67
114
 
68
115
  1. **Their words.** The title and body are the requirement. Everything else is evidence.
69
116
  2. **The screenshot.** What they saw, at their width, with their data. When the other
@@ -84,11 +131,11 @@ One decision, in their vocabulary, with the choices as `--option` flags — each
84
131
  a button. Include "hold until I check" when it is real. Batch questions per group.
85
132
 
86
133
  ```bash
87
- pikku fabric changes ask --change-id 3 --question "Make the total stand out — which way?" \
134
+ pikku changes ask --change-id 3 --question "Make the total stand out — which way?" \
88
135
  --option "Bigger" --option "Move it above the delivery line" --author-name claude-code
89
136
  ```
90
137
 
91
- Then **park it** and move on. The answer wakes `next` (same `--claimed-by`); it prints
138
+ Then **park it** and move on. The answer wakes `wait` (same `--claimed-by`); it prints
92
139
  under `Answered`, and `show` has the reply.
93
140
 
94
141
  If the answer is visual and you can build it, build each variant, screenshot all of
@@ -96,7 +143,7 @@ them in one pass at one width (baseline included), and attach them — the panel
96
143
  `--kind option` shots into a pick-one:
97
144
 
98
145
  ```bash
99
- pikku fabric changes shot --change-id 3 --label "Bigger" --kind option --image a.png
146
+ pikku changes shot --change-id 3 --label "Bigger" --kind option --image a.png
100
147
  ```
101
148
 
102
149
  `--kind evidence` is a picture that proves something, shown inline.
@@ -112,7 +159,7 @@ item's status exactly where it was:
112
159
  - you could not reproduce it — attach what you saw.
113
160
 
114
161
  ```bash
115
- pikku fabric changes reply 3 --message "Cannot reproduce on develop @ a91c4e2 — this is what I see." \
162
+ pikku changes reply 3 --message "Cannot reproduce on develop @ a91c4e2 — this is what I see." \
116
163
  --image seen.png --image-label "develop @ a91c4e2" --author-name claude-code
117
164
  ```
118
165
 
@@ -135,12 +182,11 @@ Scope names the screen they were looking at, not the file you edited.
135
182
  ## Finishing
136
183
 
137
184
  ```bash
138
- pikku fabric changes done --change-id 7 --note "What you did, for whoever reads the thread"
185
+ pikku changes done --change-id 7 --note "What you did, for whoever reads the thread"
139
186
  ```
140
187
 
141
- Branch and commit default to the checkout you are in — run it there, never type a sha.
188
+ Branch and commit default to the checkout you are in — run it there, never type a sha. `done` finds the item's commit by its `Change-Id` trailer, so close items in any order.
142
189
  An item you decided not to do is not `done`: `reply` with why and leave it for a
143
190
  human to dismiss.
144
191
 
145
- Writes need the `changes:project:write` scope; `list`, `show` and `next` without
146
- `--claim` are reads.
192
+ Registering writes with Fabric needs the `changes:project:write` scope.
@@ -4,11 +4,11 @@ description: >-
4
4
  Use when writing, reading, reorganising or validating a project's knowledge/ directory — the
5
5
  notes that say what the app is, in the language its users use. Covers the Open Knowledge Format
6
6
  note (path-as-identity markdown, YAML frontmatter, only `type` required), the app-project
7
- profile's sections (milestones, entities, decisions, questions, wishlist) and what each answers,
8
- milestone status/entities/gherkin rules, the `resource:` URI scheme tying a note to its code,
9
- what is NOT a knowledge base, and `pikku knowledge validate|index`. TRIGGER when: user asks to
7
+ profile's sections (entities, decisions, questions, wishlist) and what each answers, how notes
8
+ become changes (`pikku knowledge gaps`), the changeset plan, the `resource:` URI scheme tying a
9
+ note to its code, what is NOT a knowledge base, and `pikku knowledge validate|index|gaps`. TRIGGER when: user asks to
10
10
  write down a decision, requirement, entity or open question; asks what the app does or is; asks
11
- about knowledge/, notes, milestones, an index.md, or a diagram, callout or decision block; or
11
+ about knowledge/, notes, an index.md, or a diagram, callout or decision block; or
12
12
  hands over a product brief to record. DO NOT TRIGGER when: user asks what functions, routes,
13
13
  tables or permissions exist (that is `pikku meta` / `pikku info`, never a note), or to write a
14
14
  scenario test (use pikku-scenario).
@@ -75,7 +75,7 @@ Frontmatter fields:
75
75
 
76
76
  | Field | Meaning |
77
77
  | ------------- | ----------------------------------------------------------------------------------------------------------------------- |
78
- | `type` | **The only required field.** One of `milestone`, `entity`, `decision`, `note`, `overview`. Lowercase — every gate compares it literally, and `milestone` is the exact string `readMilestones` filters on, so a near-miss is a note no command can see. |
78
+ | `type` | **The only required field.** One of `entity`, `decision`, `note`, `overview`. Lowercase — every gate compares it literally, so a near-miss is a note no command can see. |
79
79
  | `title` | What to call the note in a listing. Falls back to the first heading, then the filename. |
80
80
  | `description` | One line, used as the note's subtitle in a section index. |
81
81
  | `resource` | Comma-separated `<kind>:<id>` URIs — the code this note is about. See below. |
@@ -91,9 +91,6 @@ Plain markdown links between notes — `[revocation](../decisions/revocation-end
91
91
  ```
92
92
  knowledge/
93
93
  index.md # type: overview — the map
94
- milestones/
95
- index.md
96
- 01-the-daily-entry.md # type: milestone
97
94
  entities/
98
95
  index.md
99
96
  entry.md # type: entity
@@ -115,7 +112,6 @@ Each section answers exactly one question, which is what lets a reader find a no
115
112
 
116
113
  | Section | The question it answers |
117
114
  | --------------------- | ------------------------------------------------------------------ |
118
- | `milestones/` | What is one buildable piece of this app, and what proves it works? |
119
115
  | `entities/` | What is this thing, in the words users use for it? |
120
116
  | `decisions/` | What was chosen, and what does that rule out? |
121
117
  | `decisions/security/` | Who may do what? |
@@ -124,36 +120,31 @@ Each section answers exactly one question, which is what lets a reader find a no
124
120
 
125
121
  **Create a section the turn you have a note for it** — never a scaffold of empty directories, and never a section without its own `index.md`. A section index says in one line what belongs in it; that sentence is the reason the file exists, so `pikku knowledge index` writes only the note listing and leaves your prose alone.
126
122
 
127
- ## Milestones
128
-
129
- A milestone is the one note type that is a piece of _work_ rather than a fact, so it alone carries state and size. It lives in `knowledge/milestones/` and nowhere else: `readMilestones` matches on that directory literally, so the same note under another section is invisible to every gate and command below.
123
+ ## Work is changes, not notes
130
124
 
131
- ````markdown
132
- ---
133
- type: milestone
134
- title: The daily entry
135
- description: An owner writes one entry per day, and sees it on the day.
136
- status: proposed
137
- entities: entry, day
138
- resource: func:createEntry
139
- ---
125
+ The base says what the app IS. What still has to be built is not written here: it is filed as
126
+ **changes** on the project's queue (`pikku changes file`), grouped into changesets, and built
127
+ one commit per change — the pikku-changes skill. A note never carries status, size or a gherkin
128
+ block; a behaviour the app must have is a sentence in the note it is about.
140
129
 
141
- # The daily entry
130
+ ```bash
131
+ pikku knowledge gaps # the notes no change builds yet
132
+ ```
142
133
 
143
- An owner writes at most one entry per day. Writing again replaces it.
134
+ `gaps` compares the notes against the plans of merged changesets and the changes already filed:
144
135
 
145
- ```gherkin
146
- Given 'owner' has no entry for today
147
- When 'owner' writes one
148
- Then it appears on today's day
149
- And writing again replaces it rather than adding a second
150
- ```
151
- ````
136
+ - **`uncovered`** — no plan covers the note.
137
+ - **`changed`** — the note was edited after the changeset that built it merged.
138
+ - **`partial`** — it merged with items deferred; they are listed.
139
+ - **`removed`** — code a merged changeset built for the note is gone from the generated meta.
140
+ - **`deleted`** — the note is gone and the code a changeset built for it is not.
152
141
 
153
- - **`status`** is `proposed` → `dispatched` → `built`. Nothing else — `MILESTONE_STATUSES` is those three, and every gate compares them literally, so an invented status fails `validate` rather than degrading. Only `proposed` is dispatchable. A profile may add one of its own ahead of `proposed` for work that is written down but must not be built yet; that is the profile's to define and validate, not core's.
154
- - **`statusAt:` and `attempts:` are bookkeeping, not content — never hand-edit them.** A loop driving this base writes both. `statusAt:` is stamped by whatever moved the status, and is what makes "how long has this been building?" answerable; the file's mtime is not the transition time, because a note is edited after dispatch for all sorts of reasons. `attempts:` is `seat@hash` entries recording which seat has already tried to move this note forward, against the content it was trying to move — it is the loop's only brake, and clearing it by hand hands back a budget that exists to stop a note nothing can satisfy being rewritten forever. Rewriting the note's real content refunds that budget on its own, which is the point: an answer that changes the note is what unsticks it.
155
- - **`entities`** lists what the milestone touches, **at most three**. Past three it is not one buildable piece — split it.
156
- - **The scenario is a fenced `gherkin` block, in the third person.** `Given 'owner' has no entry` — never `Given I have no entry`. A quoted word _means a persona_, which is what lets a reader (and a test) tell who is acting. First person hides that, so it is rejected. The console draws the keywords as a column and each quoted persona as a chip, so a first-person scenario is visibly a block with no personas in it.
142
+ The first three are work: file a change for each, and end its body with the gap's
143
+ `Knowledge: <note>@<hash>` line so it is not filed again. The last two mean the code and the
144
+ knowledge disagree about something that was built — read `git log` for who removed it and why, then
145
+ either bring the knowledge into line (edit or delete the note, or file a change that removes the
146
+ code) or file a change that restores it. When you cannot tell which, file the change and ask on it.
147
+ `pikku changes next` hands open gaps to a knowledge agent when there is nothing else to do.
157
148
 
158
149
  ## Showing it
159
150
 
@@ -230,7 +221,7 @@ These are all things that exist somewhere better, so a note is always the copy t
230
221
  | Do not write | Because it lives in |
231
222
  | ----------------------------------- | ---------------------------------------------------------- |
232
223
  | a `personas/` section | `definePersonas()` in the project's own code |
233
- | a `scenarios/` section | the gherkin block inside the milestone it belongs to |
224
+ | a `scenarios/` section | the `scenarios` of the plan for the changeset that builds it |
234
225
  | a `permissions/` section | a decision note under `decisions/security/` |
235
226
  | a list of tables, columns or routes | `pikku meta` — the generated schema _is_ the schema |
236
227
  | a changelog | `CHANGELOG.md` at the repo root |
@@ -249,66 +240,35 @@ pikku knowledge index # refresh every index.md
249
240
  pikku knowledge index --check # report stale indexes without writing (CI gate)
250
241
  ```
251
242
 
252
- `validate` reports: notes with no `type`, a missing `knowledge/index.md`, a section with no `index.md`, notes flat at the root, sections that duplicate what the project already declares, milestones with a bad or missing `status`, milestones over three entities, milestones with no gherkin block or a first-person one, `decision` fences that state no `chosen:` or rule nothing out, and every `resource:` that no longer resolves. Errors fail the command; warnings do not.
243
+ `validate` reports: notes with no `type`, a missing `knowledge/index.md`, a section with no `index.md`, notes flat at the root, sections that duplicate what the project already declares, plans under `knowledge/plans/` that do not read or contradict themselves, `decision` fences that state no `chosen:` or rule nothing out, and every `resource:` that no longer resolves. Errors fail the command; warnings do not.
253
244
 
254
245
  `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.
255
246
 
256
- ### What to do next
257
-
258
- ```bash
259
- pikku knowledge next # the one thing to do next, derived from what is on disk
260
- ```
261
-
262
- `next` is a pure read: it looks at the notes and answers with exactly one action —
263
- `repair-note`, `write-plan`, `ask-user`, `dispatch`, `hold`, or `idle`. Nothing has to
264
- be armed by whoever noticed a transition, so calling it twice is free and a state
265
- nobody anticipated is a missing answer rather than a run that quietly stops.
266
-
267
- Two things about the output matter if you are driving it:
268
-
269
- - **`reason` is machine wording.** It names the note, the frontmatter key and what the
270
- gate wanted. Never repeat it to a person — they have not seen a note and it will read
271
- as gibberish about files.
272
- - **`ask-user` carries a `question` as well.** That IS the version for a person: a
273
- `header`, the question in the language of their app, and `options` when the answer
274
- comes from a closed vocabulary (which `status:` it is, which `surface:` it is).
275
- `options` is empty when the answer is free text, and an empty list means offer free
276
- text — never invent choices to fill it.
247
+ ### The changeset plan
277
248
 
278
- `hold` means a profile's own gate is holding the milestone and nothing this loop knows
279
- about can clear it. It names the hold and the notes it is about; what to do then
280
- belongs to that profile, not here.
281
-
282
- ### The milestone plan
283
-
284
- 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
+ A changeset that creates or alters a table, or that the judge says needs one, is planned before any
250
+ of its code. The **plan** — JSON at `knowledge/plans/<changeset>.plan.json`, committed on the
251
+ changeset's branch — says what has to exist for it, which notes it `covers`, and is what a finished
252
+ build is measured against:
285
253
 
286
254
  ```bash
287
255
  pikku knowledge plan schema # the format, in full
288
- pikku knowledge plan set <milestone> <file> # validate and write it
289
- pikku knowledge plan show <milestone> --for-build # the ordered work a build follows
290
- pikku knowledge plan progress <milestone> # what it still owes, read from .pikku/
291
- pikku knowledge plan defer <milestone> <item> -r "<why>"
256
+ pikku knowledge plan set <changeset> <file> # validate and write it
257
+ pikku knowledge plan show <changeset> --for-build # the ordered work a build follows
258
+ pikku knowledge plan progress <changeset> # what it still owes, read from .pikku/
259
+ pikku knowledge plan defer <changeset> <item> -r "<why>"
292
260
  ```
293
261
 
294
- `progress` reconciles the plan against pikku's generated meta — set membership, never anyone's status — and exits non-zero while the first pass is short, or while anything already built contradicts the plan. Unbuilt work in a later pass is reported, not blocked; a function that shipped wide open against a planned permission rule blocks from any pass, because that is a hole rather than a backlog. How a plan is written is `pikku-architect`; building against one, and the order plan-then-build, is `pikku-build`.
295
-
296
- ### A finished milestone is a tombstone
297
-
298
- **Once a milestone reaches `built`, its note and its plan are closed. Do not edit either.** Not to correct the wording, not to fold in what the build actually turned out to need, not to add the item everyone agrees should have been there. A finished milestone is the record of what was agreed and what was measured against it, and a record that can be revised afterwards measures nothing.
299
-
300
- This is the rule the shape of the thing already implies. `progress` reconciles a plan against generated meta and fails when what shipped contradicts it — a check with no force at all if the losing side of the contradiction may simply be rewritten. `attempts:` brakes a note nothing can satisfy, and refunds that budget when the note's content really changes; a `built` note that keeps changing is that brake removed. Both only work while the plan stays still.
262
+ `progress` reconciles the plan against pikku's generated meta — set membership, never anyone's status — and exits non-zero while the first pass is short, or while anything already built contradicts the plan. Unbuilt work in a later pass is reported, not blocked; a function that shipped wide open against a planned permission rule blocks from any pass, because that is a hole rather than a backlog. How a plan is written is `pikku-architect`; building against one is `pikku-changes`, whose `done` refuses the last change of a planned changeset until `progress` is clean.
301
263
 
302
- So when a `built` milestone turns out to be wrong or incomplete, **the answer is always a new note, never an edit to the old one**:
264
+ ### A merged plan is a tombstone
303
265
 
304
- - It needed more than it said → a new milestone, which may name the old one.
305
- - It was built differently than planned → that is what `progress` is for. Reconcile forward, or record a decision saying why the plan was not the right shape.
306
- - It was simply wrong → a decision note that supersedes it. The wrong milestone stays where it is; a base whose history is edited cannot answer *why* anything is the way it is, which is most of what a base is for.
266
+ **Once a changeset merges, its plan is closed. Do not edit it.** Not to correct the wording, not to fold in what the build actually turned out to need. A merged plan is the record of what was agreed and what was measured against it, and a record that can be revised afterwards measures nothing.
307
267
 
308
- The exception, and it is narrow: bookkeeping the loop owns. `statusAt:` and `attempts:` are written by whatever moved the note, at any status, and are bookkeeping rather than content. Nothing else about a `built` note moves again.
268
+ Notes are different: they say what the app is now, so they change whenever it does. Editing a note a merged changeset covered is exactly how new work arrives — `gaps` reads it as `changed`, and it is filed again.
309
269
 
310
270
  ## Profiles built on this one
311
271
 
312
272
  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.
313
273
 
314
- Fabric is the one that exists. It adds `decisions/design/` — rules about how the app looks and behaves — a `screens/` section, and a `design:` field on a milestone pointing at the design options it was built from. Both are Fabric's to validate; `pikku knowledge validate` passes them through untouched. Everything else in this skill is the same in both.
274
+ Fabric is the one that exists. It adds `decisions/design/` — rules about how the app looks and behaves — and a `screens/` section. Both are Fabric's to validate; `pikku knowledge validate` passes them through untouched. Everything else in this skill is the same in both.
@@ -79,6 +79,16 @@ service's paths and size limit; omitting it does not disable it.
79
79
  function enriches the event with the function id, wire type, trace id and user
80
80
  identity; an `insertInto('audit_log')` of your own gets none of that, and a
81
81
  write inside a transaction that later rolls back records nothing.
82
+ - **Do not reach for `node:os` (`tmpdir()`, `homedir()`) or hand-roll `mkdtemp`
83
+ to get a scratch file.** `node:os` is missing from the Cloudflare Workers
84
+ runtime, so one import fails the whole worker at upload, even if the code path
85
+ never runs there. `TemporaryFileService` (`@pikku/core/services/temporary-file-service`)
86
+ is a **wire service**, not a singleton: build it in `pikkuWireServices` with
87
+ `new TemporaryFileService(singletonServices.logger, tempDir).createInstance()`
88
+ and return the `TemporaryFileInstance`, so each invocation owns its own files.
89
+ Use `writeFile(key, stream)`, `getTempFileAbsolutePath(key)` when a subprocess
90
+ needs a path, and `cleanup()` in a `finally`. Do not construct it in
91
+ `pikkuServices` and close over it in a singleton — pass the instance in per call.
82
92
  - **Do not log an unrevealed secret.** Every logger argument is `Safe<>`-guarded,
83
93
  so a `SecretValue` nested anywhere in the call collapses it to `never` and it
84
94
  stops compiling. That is deliberate — it would have printed `[secret]` anyway.
@@ -155,7 +155,11 @@ public because receivers share the scheme:
155
155
  ```ts
156
156
  const raw = await request.text()
157
157
  if (
158
- !webhookService.verify(secret, request.headers.get('x-pikku-signature')!, raw)
158
+ !(await webhookService.verify(
159
+ secret,
160
+ request.headers.get('x-pikku-signature')!,
161
+ raw
162
+ ))
159
163
  ) {
160
164
  throw new UnauthorizedError()
161
165
  }
@@ -102,7 +102,7 @@ const tokens = await exchangeSlackOAuthCode({
102
102
  ```typescript
103
103
  import { verifySlackSignature } from '@pikku/gateway-slack'
104
104
 
105
- verifySlackSignature(signingSecret, signature, timestamp, body): boolean
105
+ verifySlackSignature(signingSecret, signature, timestamp, body): Promise<boolean>
106
106
  ```
107
107
 
108
108
  **Signature before timestamp** — the two middle arguments are both strings, so
@@ -158,9 +158,10 @@ subscribe to its events as `<source>:<event>`:
158
158
  provider's private key; the stored secret is its PEM public key (Wise).
159
159
  - Anything else (a timestamp in the signed payload, a signature in the body
160
160
  or query, a URL in the signed string) is a function
161
- `(request, secret, services) => boolean`, built from `hmacDigest`,
161
+ `(request, secret, services) => boolean | Promise<boolean>`, built from `hmacDigest`,
162
162
  `verifyHmacSignature`, `verifyPublicKeySignature` and
163
- `timingSafeStringEqual` in `@pikku/core/hmac`.
163
+ `timingSafeStringEqual` in `@pikku/core/hmac` (all but the last are
164
+ async: `await` them).
164
165
 
165
166
  A request with a body that fails is refused with a 401. A bodiless request
166
167
  that fails (a validation token in the query) still reaches