@akanjs/cli 3.0.0-alpha.99 → 3.0.0-beta.1

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 (71) hide show
  1. package/.build-stamp +1 -1
  2. package/{abstractCompactor-byytxs22.js → abstractCompactor-mjh5ppsq.js} +1 -1
  3. package/{agent.command-v1a4746v.js → agent.command-d2gfxfck.js} +5 -5
  4. package/{application.command-j05fr5jc.js → application.command-pj2m7584.js} +5 -5
  5. package/{applicationBuildRunner-s4gsk728.js → applicationBuildRunner-kxwbssgn.js} +2 -2
  6. package/{applicationReleasePackager-28v9vbzk.js → applicationReleasePackager-4yasxwv9.js} +1 -1
  7. package/buildBatch.proc.js +2 -2
  8. package/{capacitorApp-8d6d4zkm.js → capacitorApp-36rbr858.js} +2 -2
  9. package/{cloud.command-r6wvq2rp.js → cloud.command-jshwdq22.js} +9 -9
  10. package/{context.command-4vdt1aqq.js → context.command-ez8a0zyz.js} +13 -13
  11. package/{devTui-tnj2hh4f.js → devTui-40ggmd99.js} +85 -84
  12. package/{devTuiView-xzed2pd6.js → devTuiView-c08ccbat.js} +1 -1
  13. package/{guideline.command-p92dq6ts.js → guideline.command-xmqwx8zm.js} +11 -7
  14. package/guidelines/agentRule/agentRule.instruction.md +112 -32
  15. package/guidelines/conventions/conventions.instruction.md +100 -40
  16. package/guidelines/mcpRule/mcpRule.generate.json +11 -1
  17. package/guidelines/mcpRule/mcpRule.instruction.md +296 -53
  18. package/guidelines/queryRule/queryRule.instruction.md +17 -9
  19. package/guidelines/ssrRule/ssrRule.instruction.md +15 -13
  20. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +10 -5
  21. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +68 -3
  22. package/incrementalBuilder.proc.js +1 -1
  23. package/{index-gq44bqjt.js → index-259dsnz3.js} +2 -2
  24. package/{index-b9rkcrx8.js → index-2hq52jq3.js} +9 -7
  25. package/{index-qsq3q8fy.js → index-2tv3fdx4.js} +21 -10
  26. package/{index-qkk27bk6.js → index-4edzf1gr.js} +4 -4
  27. package/{index-yza7zkpt.js → index-600zhnve.js} +10 -10
  28. package/{index-srdssqj7.js → index-7737gxcp.js} +3 -3
  29. package/{index-zmjppp25.js → index-8pm30m73.js} +5 -5
  30. package/{index-p5neav9b.js → index-a64esmtb.js} +54 -9
  31. package/{index-0amdxycf.js → index-fz5q3gd7.js} +3 -3
  32. package/{index-fc1bh0a7.js → index-gyd220p2.js} +1 -1
  33. package/{index-5svjba73.js → index-j9yhg404.js} +1 -1
  34. package/{index-c69j78kk.js → index-kn1h7p1x.js} +1 -1
  35. package/{index-gtmz74tj.js → index-ktb5njgy.js} +2 -2
  36. package/{index-znynzt5w.js → index-mak3cd28.js} +1 -1
  37. package/{index-29sxqfaw.js → index-ne2qy0nk.js} +3 -3
  38. package/{index-hyywke0f.js → index-pnqbkf5p.js} +1 -1
  39. package/{index-8p8p0cf8.js → index-rwg2aa4f.js} +3 -3
  40. package/{index-5wd728df.js → index-v0qh106m.js} +215 -12
  41. package/{index-7d6vny25.js → index-ws5e0j51.js} +2 -2
  42. package/{index-1yxtbmbj.js → index-wt7qrtd4.js} +1 -1
  43. package/{index-gkkdx9kp.js → index-zry5nz81.js} +6 -6
  44. package/index.js +19 -19
  45. package/{library.command-s4fszngs.js → library.command-nbsky672.js} +3 -3
  46. package/{localRegistry.command-fq2w7cgz.js → localRegistry.command-yeq57wjr.js} +8 -8
  47. package/{module.command-6n7dwvav.js → module.command-29vqzrh8.js} +6 -6
  48. package/{package.command-gr82ypnr.js → package.command-axv0abmn.js} +3 -3
  49. package/package.json +2 -2
  50. package/{page.command-7gvgb3jb.js → page.command-vs9s3zrh.js} +3 -3
  51. package/{primitive.command-tk5ckw38.js → primitive.command-cbxmqbtc.js} +7 -7
  52. package/{quality.command-38v81baf.js → quality.command-gmbs7nws.js} +2 -2
  53. package/{repair.command-5xbwvsxp.js → repair.command-bqsjptsj.js} +5 -5
  54. package/{routeSourceValidator-ze6x894c.js → routeSourceValidator-apyqw7wk.js} +90 -17
  55. package/{scalar.command-0epwcr5k.js → scalar.command-6k1vhe9s.js} +5 -5
  56. package/{subspace.command-e2t36xb2.js → subspace.command-fwjfcts7.js} +7 -5
  57. package/templates/app/page/_index.tsx +3 -3
  58. package/templates/app/page/_layout.tsx +31 -32
  59. package/templates/appSample/page/task/[taskId]/_index.tsx +22 -27
  60. package/templates/appSample/page/task/[taskId]/edit.tsx +25 -30
  61. package/templates/appSample/page/task/_index.tsx +3 -7
  62. package/templates/appSample/page/task/_layout.tsx +4 -15
  63. package/templates/appSample/page/task/new.tsx +4 -10
  64. package/templates/crudPages/[__model__Id]/edit/page.tsx +26 -28
  65. package/templates/crudPages/[__model__Id]/page.tsx +20 -25
  66. package/templates/crudPages/new/page.tsx +24 -23
  67. package/templates/crudPages/page.tsx +25 -24
  68. package/templates/crudSinglePage/page.tsx +25 -24
  69. package/templates/lib/sig.ts +4 -0
  70. package/{workflow.command-kphbr7eh.js → workflow.command-nwpcdc8p.js} +10 -10
  71. package/{workspace.command-ag5hy887.js → workspace.command-8s050m0h.js} +20 -20
@@ -72,9 +72,22 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
72
72
  fills whichever adaptor holds `LlmAdaptorRole`, reaching it as the `llmOption` use. The settings are the role's
73
73
  rather than one provider's, so they survive a swap. **DeepSeek is the built-in default** (`deepseek-v4-flash` at
74
74
  `https://api.deepseek.com`); with no `apiKey` the app still boots and the chat answers `llmUnavailable`. Swap
75
- providers the way middleware is applied: `option.applyAdaptor(LlmAdaptorRole, ClaudeLlm)`, where the
76
- implementation is an `adapt()` class in a `srvkit/` implementing `LlmAdaptor.chat(request, onDelta?)` — ignore
77
- `onDelta` and the chat still answers whole.
75
+ providers the way middleware is applied: `option.applyAdaptor(LlmAdaptorRole, AnthropicLlm)`. An app may still
76
+ write its own — an `adapt()` class in a `srvkit/` implementing `LlmAdaptor.chat(request, onDelta?)`, where
77
+ ignoring `onDelta` still answers whole — but for vision it no longer has to.
78
+ - **Three providers ship, and only two of them read a picture.** `DeepseekLlm` is the default and declares no
79
+ `accepts`, which is a fact about its API rather than a gap. `OpenaiLlm` speaks the same chat-completions dialect
80
+ — `OpenaiDialect`, shared between them so a protocol fix lands once — against `https://api.openai.com/v1` and
81
+ declares `{ image: true }`, sending an image as a content part. `AnthropicLlm` is the Messages API and declares
82
+ `{ image: true, document: true }`; it is its own file rather than a branch, because the system prompt is a field
83
+ and not a message, tool calls and results are content blocks, the results ride in a *user* turn, roles must
84
+ alternate — so two wire turns that map to one role are merged — and `max_tokens` is required. **Both new ones
85
+ require `model`**: a default would age into a 404, and worse, it would decide the vision claim for the app.
86
+ - **`accepts` is answered per model, through `option.setLlm({ accepts })`.** An adaptor answers for an API and one
87
+ API serves models that differ, so the override rides beside the `model` it is a fact about. It is not a table
88
+ the framework keeps: a table is a claim about models that ship after it, and getting this wrong is the worst
89
+ failure available — a provider handed bytes it cannot decode either refuses the turn or accepts it having seen
90
+ nothing, and the model then answers confidently about a file it never read.
78
91
  - **An adaptor answers `null` for "not configured" and *throws* for a refusal it can explain.** The two are
79
92
  different things to be told: collapsing both into `null`, the way the adapter convention otherwise reads, left a
80
93
  user reading `llmUnavailable` — "no model is configured" — about a conversation that had merely outgrown the
@@ -92,15 +105,28 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
92
105
  prompt. **What the provider cannot read is replaced by a note naming the file**, never dropped: an attachment the
93
106
  model never saw is one it answers about from the filename. An adaptor declares `accepts: { image, document }` and
94
107
  `AgentService.readable` degrades the rest, so a text-only provider needs no attachment code at all — DeepSeek
95
- declares none, which is why an image against the default provider is refused out loud while an extracted PDF
96
- works, `text` being readable by every model there is. **A `prompt()`'s `Msg.image` is the same wire shape** and
97
- reaches the chat as an attachment rather than the literal `[image]` it used to become. Persisting keeps each
108
+ declares none, which is why an image against the *default* provider is refused out loud while an extracted PDF
109
+ works, `text` being readable by every model there is. Swap in `OpenaiLlm` or `AnthropicLlm` and the same image
110
+ arrives as an image. `accepts.document` is one boolean over every non-image type, so an adaptor whose API has a
111
+ block for only some of them — Anthropic's `document` is PDF and nothing else — names the rest in the text rather
112
+ than dropping them, which is the wire's own rule applied at the one layer that knows which blocks exist. Persisting keeps each
98
113
  attachment's name and drops its content: web storage is a few megabytes, one screenshot fills a chunk of it, and
99
114
  a save that fails is silent — so keeping the bytes would quietly stop keeping the transcript.
100
115
  **The ceilings are the message's, not the file's**: 4 MB per file, 8 MB and five files per message, and the same
101
116
  file twice is refused by name. The bytes ride inside one turn's JSON, so what a provider refuses is the sum —
102
117
  and a request that cannot be sent is one the user has to empty the composer to escape, which is why the refusal
103
- happens at the paperclip and names the file it dropped.
118
+ happens at the paperclip and names the file it dropped. All three are defaults rather than the law —
119
+ `<Agent.Chat attachLimits={{ perFileBytes, perMessageBytes, perMessageCount }} />` — because what one request can
120
+ carry belongs to the configured provider and not to the framework. The per-file one is measured on **what the
121
+ reader produced**, not on the file that was picked: a reader that uploads and answers a `url` has already paid
122
+ what the ceiling exists for, so refusing its 6 MB photo would refuse it for a cost it does not incur. Only the
123
+ built-in path checks the source size, and there the file itself is the carrier.
124
+ - **`MessageAttachment.ref` is the host's own handle on the file — carried, never read.** A file id, a storage key,
125
+ whatever turns the attachment back into something a tool can be handed. The framework only moves it: the model is
126
+ never shown one, and a tool the app publishes (`useFileFieldTool` below) is what spends it. Without it a host that
127
+ stores its uploads keeps a map beside the transcript keyed on name and size — the same guess `Attachment.same`
128
+ has to make, and the one that is wrong for two crops of one export. So when both sides carry a `ref`, it *is* the
129
+ answer to "the same file twice", and the heuristic is what is left for when they do not.
104
130
  - **Speech is one engine contract and the framework's own policy.** `<Agent.Chat voice={engine} />` takes a
105
131
  `VoiceEngine` — `listen(handlers)` and `speak(sentence)`, both cancellable — and the chat decides everything
106
132
  else: a press-to-talk microphone whose transcript lands in the composer to be corrected, one utterance per
@@ -131,6 +157,11 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
131
157
  `data-agent-zone` container; guides follow the layout cascade (ancestors and own, never a sibling's). Zone
132
158
  membership is positional — there is no per-declaration zone key, so a lib component joins whatever zone the app
133
159
  mounts it in.
160
+ - **Two agents' turns run in parallel; their tool calls take turns.** Tool execution is serialized across the
161
+ whole page, because `AgentAbort` and `AgentProgress` reach the running call through a module slot rather than a
162
+ parameter — two calls in flight restore each other's slot and the second one's progress goes nowhere. Only the
163
+ execution queues: a zone parked on an approval card holds nothing up, since waiting for the user sits outside
164
+ the queue, and a call that is aborted releases it.
134
165
  - **Everything a zone publishes is named `<id>.<name>`,** and that name is what instructions must use. A bare
135
166
  `createVideoProject` inside a zone naming its tool `videoProjectDraft.createVideoProject` is a tool that does
136
167
  not exist: the model calls it, the surface answers `Unknown tool`, and a turn is gone. Build the name from the
@@ -200,7 +231,12 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
200
231
  globally and a component cannot build one per render: pass the list it has — a slice's sort keys, the options a
201
232
  prop carried — and it is published and enforced the same way. Neither reaches a set that fills in *after* the
202
233
  first render, since a declaration is mount-static; put that in the tool's `guard`, which is re-read per call and
203
- can name the current values in its refusal. **An argument type nothing can describe — a model class, `Any`, a
234
+ can name the current values in its refusal. **One array level of either is describable too** —
235
+ `.arg("bodies", [String])`, `.opt("modes", [TaskStatus])` — and `oneOf` then narrows the elements rather than the
236
+ list. It is the schema `fill<Model>Form` already builds for an array field, so the same shape reaching an agent
237
+ through a form and not through a component tool was an oversight: a tool that could not say "a list of these"
238
+ said it in prose, and the app then owned a parser for the format it invented. A second level is not describable.
239
+ **An argument type nothing can describe — a model class, `Any`, a
204
240
  `Map` — withdraws the whole tool and says so on the console**, naming the tool, the argument and the type; the
205
241
  callable still drives the click a person makes. It does not throw: a tool schema is built during render, and an
206
242
  agent-tooling mistake that aborted the render would cost the route its server rendering. `st.useState`'s `set`
@@ -315,6 +351,29 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
315
351
  for a person too: the options arrive when the dropdown opens, and an agent never opens it. `Field.ParentId` /
316
352
  `Field.ChildrenId` need none of that — the id *is* the value, so the ordinary setter describes it. All four still
317
353
  require the setter **by reference**, and a `disabled` picker publishes nothing.
354
+ - **An upload control reaches an agent the same way, through `useFileFieldTool`.** The clause after "picked **or
355
+ uploaded**" has its own hook: the control hands `read` and `label` and gets `load<Field>OptionsOn<Model>` — the
356
+ same name the relation picker publishes, because a form draws both side by side and a model that learned the
357
+ name on one field spells it the same on the next — plus the field's own `set<Field>On<Model>` taking
358
+ `<field>Id` / `<field>Ids`, with `add`/`sub` besides on an array field. **`read` omitted publishes nothing**: a
359
+ shared `Field.Img` forwards an optional prop, and a call site handing it none has no candidates and must not
360
+ grow a tool that refuses every id. `read` is the control's rather than the framework's because the ids are the
361
+ host's — a conversation carries `MessageAttachment.ref`s, and only whoever set them can turn one back into the
362
+ model a form field holds. akanjs draws no upload control itself, so this is the hook a lib's `Field.Img` /
363
+ `Field.Imgs` calls, exactly as `Field.Relation` calls the relation one.
364
+ - **`add`/`sub` on a file field are the only way in, not the weaker way.** For every other list of ids they are a
365
+ convenience the framework declines to pay a tool for — `useFieldTool` publishes them for embedded rows only, and
366
+ the relation picker not at all, because a picker's `read` is the whole resolvable set so rewriting the array is
367
+ always expressible. An upload control's `read` is what *this conversation* brought, and the field may already
368
+ hold files that predate it: naming the array whole drops those, since the ids that would keep them are ids the
369
+ guard has never heard of. **`max` / `min` are passed, not derived** — `ConstantField` carries `arrDepth`, which
370
+ says the value is a list and not how long a legal one is — and they reach the tool's *description* as well as
371
+ its guard, because a limit an agent can only learn by tripping the server's `Err` is the round trip the listing
372
+ tool exists to spare it. Two call sites rendering one field with different caps describe one name two ways, and
373
+ the surface warns, which is the right answer: a field has one cap. For the same reason the unknown-id refusal
374
+ **names the boundary rather than the absence** on a list: an id already on the field is real, on screen, and in
375
+ the form the agent just read, so "no such file" reads as the form being wrong instead of the verb — it says the
376
+ id is not among what is *offered*, and points at `sub<Field>On<Model>`, which does cover it.
318
377
  - **An array of embedded rows also publishes `add<Field>On<Model>` and `sub<Field>On<Model>`** — append, and
319
378
  remove-by-position — beside the whole-array setter. Not new authority: the setter can already produce any array
320
379
  those two can, so they are strictly weaker. What they add is that neither can touch a row it was not given, and
@@ -367,18 +426,11 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
367
426
  useless to a model — that is cheaper than every tool learning to avoid it.
368
427
 
369
428
  ## Slash Commands And The Transcript
370
- - **`prompt()` endpoints double as the chat's slash commands.** There is no listing endpoint — the client reads
371
- its own serialized signals — so a prompt's dictionary `.desc()` is what the menu shows, and its guards are
372
- enforced by the prompt's own GET at call time. Arguments are positional and whitespace-separated, and quoting
373
- is how a sentence stays one of them (`/reviewTask t1 "look at the totals"`) — a prompt taking a single `String`
374
- is the common case, and an unquoted sentence would fill its second parameter with the second word.
375
- - **The chat answers six slash commands of its own**, listed in the same `/` menu ahead of the prompts:
376
- `/new` (`/clear`), `/retry`, `/compact`, `/copy`, `/help` and `/tools`. An app writes none of them and cannot add
377
- one — the extension point for a product's own command is a `prompt()` endpoint, which is guarded and server-side.
378
- **A built-in wins a name collision with a prompt of the same name**, the mirror image of the tool rule: a
379
- component's `st.tool` shadows a built-in it means to replace, but no library's prompt may take `/new` away from
380
- the user who typed it — so a shadowed prompt is dropped from the menu rather than listed twice. `/new` and
381
- `/copy` are also dispatched *before* the is-a-turn-running check **and before the question card takes the
429
+ - **The `/` menu is the chat's own.** An app's prompts are `page().prompt()` declarations served over MCP to
430
+ external agents; a browser chat lists none of them, so the menu holds the six built-in commands and nothing else.
431
+ - **The chat answers six slash commands of its own**: `/new` (`/clear`), `/retry`, `/compact`, `/copy`, `/help` and
432
+ `/tools`. An app writes none of them and cannot add one — a workflow a person invokes by name belongs to MCP, as
433
+ a `page().prompt()`, where it is guarded by the page it opens. `/new` and `/copy` are dispatched *before* the is-a-turn-running check **and before the question card takes the
382
434
  composer**, because mid-turn is exactly when they are reached for and a question the agent asked is the middle
383
435
  of a turn like any other — answered as text, `/new` would have reached the model as the user's decision.
384
436
  `/new` therefore aborts the turn it is clearing and waits for it to wind down, since the loop clears its own
@@ -438,21 +490,24 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
438
490
  row, Tab completing its name, and Escape closing the menu and then, pressed again, the panel.
439
491
 
440
492
  ## Built-In Tools
441
- - The framework publishes five built-ins on every store surface: `navigate` (internal paths only, the same
442
- router `Link` rides), `goBack` (this session's history — global, because history is not a control a page owns and
443
- a page that draws no back link is not one you may not leave — a screen that must not be left withholds them
444
- with `builtins`, not with prose), `readScreen` (the rendered DOM as compact text —
445
- headings, links, control values, and `(disabled)` on a control or button that has it; the chat's own UI is
446
- skipped via `data-agent-ui`, and a password value is never read), `readState(key)` (one masked store key), and
447
- `highlight(target)`. Declaring a hook tool under one of those names shadows the built-in, so reuse them only to
448
- mean that. **There is no general-purpose wait**: a built-in one was reachable on every screen and a model spent
449
- it on whatever key it liked, parking turns nobody asked to park. Waiting belongs to the screen that knows what
450
- is worth waiting for — publish an `st.tool` beside the control that starts the work, and let it await the work.
493
+ - The framework publishes five built-ins on every store surface: `navigate` (internal paths only, the same router
494
+ `Link` rides), `goBack` (this session's history — global, because history is not a control a page owns and a page
495
+ that draws no back link is not one you may not leave — a screen that must not be left withholds them with `builtins`,
496
+ not with prose), `readScreen` (the rendered DOM as compact text — headings, links, control values, and `(disabled)`
497
+ on a control or button that has it; the chat's own UI is skipped via `data-agent-ui`, and a password value is never
498
+ read), `readState(key)` (one masked store key — a key that is unknown or that this screen does not read is refused
499
+ *with the keys it can* read, the way `readScreen` names its sections), and `highlight(target)`. Declaring a hook tool
500
+ under one of those names shadows the built-in, so reuse them only to mean that. **There is no general-purpose wait**:
501
+ a built-in one was reachable on every screen and a model spent it on whatever key it liked, parking turns nobody
502
+ asked to park. Waiting belongs to the screen that knows what is worth waiting for — publish an `st.tool` beside the
503
+ control that starts the work, and let it await the work.
451
504
  - **A tool that changes the screen waits for the screen before it answers.** `router.push` returns while the RSC
452
505
  payload is still in flight and a store action that fires `void fetch.*` commits a tick later, so `navigate`
453
506
  awaits `ScreenSettle.wait()` — DOM quiescence, bounded, because the client router hands its promise to nobody —
454
- and the session awaits it after every non-`query` tool before taking the change report. Without it the report
455
- describes the moment before the change landed and the `readScreen` that follows reads the page the user left.
507
+ and the session awaits it after every tool that did not declare `settle: false` before taking the change report.
508
+ Without it the report describes the moment before the change landed and the `readScreen` that follows reads the
509
+ page the user left. The reading built-ins (`readScreen`, `readState`, `highlight`) declare it, because a wait
510
+ costs 120ms of quiet at the very least and they change nothing a resource holds.
456
511
  New tools and state from a fresh route are still only listed from the next turn: the catalogue is snapshotted
457
512
  when the turn starts.
458
513
  - **A tool that waits for its own work costs no model turns; one that returns early costs one round trip per
@@ -483,6 +538,13 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
483
538
  one costs nothing, while two buttons reading "Save" are not the same control. **Nothing hidden ever resolves** —
484
539
  a ring nobody can see reads as a broken tool, not as a miss. A section named by a heading is read to the next
485
540
  heading of its level or higher.
541
+ - **An image is always named, and its address rides only when asked for.** `readScreen` prints `[image: <alt>]`,
542
+ and `[image]` for one with no `alt` — dropping the unlabelled one left a card that renders a picture reading
543
+ exactly like a card that renders nothing, and "this page shows no image" is the one answer the screen cannot
544
+ support. `readScreen({ images: true })` appends `(<src>)` to each, for handing one to a tool that takes a
545
+ picture; it is off by default because a gallery is one long URL per thumbnail, which is the read's whole budget
546
+ spent on the part of the screen it can say the least about. A `data:` URL is never printed — that is the bytes
547
+ themselves rather than somewhere to fetch them.
486
548
  - **A screen is only aimable if its names are printed.** `readScreen` writes `(#anchor)` beside a heading that
487
549
  opens an id'd or scoped container, and a truncated read ends with the headings below the cut — otherwise
488
550
  everything past the 8000-character limit is unreachable, because nothing names it, and an agent asked to point
@@ -519,6 +581,24 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
519
581
  a no-op when nobody is rendering it. The chat shows it on that call's row until the row resolves. It is the
520
582
  browser twin of `McpProgress.report`. Import it and `AgentAbort` from `akanjs/store`: an app may not reach
521
583
  `use-agentic` directly (`no-import-external-library`), and those two are the channels a long tool body needs.
584
+ - **One turn carries every call the model made in it, and they run in order.** An assistant turn may hold any
585
+ number of tool calls; the session executes them one at a time, in the order they arrived, and posts them back as
586
+ one `tool` message. A batch therefore costs **one** model round trip, where the same calls chained one per turn
587
+ cost a round trip *and* a resend of the whole transcript each — and then meet the turn cap halfway through. The
588
+ framework asks for the batch in its own half of the system prompt (`AgentService.preamble`); what an app adds is
589
+ a tool that does the whole job in one call, so there are fewer calls to batch in the first place. Two agents'
590
+ calls still take turns page-wide — see Zones.
591
+ - **A chain of ten calls is usually a model checking its own work, not ten things to do.** Measured against the
592
+ provider, the batch itself lands in one turn; what follows it is `readScreen → readState → readScreen`, one read
593
+ per turn, confirming a write whose change report already said the same thing. The preamble tells the model not
594
+ to, which is worth a turn and a half on a plain "approve what is pending". So a tool that returns *what
595
+ changed* pays for itself twice — once as the answer, once as the reads it stops. The read a fresh route needs
596
+ after `navigate` is acquisition rather than confirmation and is unaffected.
597
+ - **A batch reports each changed resource once, at the end.** Every call takes its own before-and-after, so eight
598
+ approvals would each attach the whole list they approved from — and by the time the model reads the message,
599
+ only the last copy is still true. The superseded ones are dropped (`ToolOutput.deduped`), so a resource several
600
+ calls touched appears once, as it finally stands, on the last result that touched it. What each call did is
601
+ still its own `result`; do not compensate by returning the collection from every write.
522
602
  - **The turn cap is a question, not a dead end.** At `maxTurns` the session asks whether to keep going through the
523
603
  same card `askUser` uses, and the answer rides as the user's own turn — so a steer typed instead of the
524
604
  keep-going choice reaches the model as guidance. A host that renders no `pendingQuestion` passes no
@@ -403,7 +403,9 @@ framework has no hard delete for a model table, and `delete` is deliberately lef
403
403
  The facade spells `Many`/`One` out on its writes (`updateOne` / `updateMany` / `removeOne` / `removeMany`); only the
404
404
  count was shortened to `count(query)`, with `countDocuments` kept as `@deprecated`. `updateById(id, update)` and
405
405
  `removeById(id)` are those same query-level writes narrowed to one id, **not** the document path: they fire no hooks,
406
- so a model whose removal cascades or carries a `_postRemove` still goes through `remove<Model>(id)`.
406
+ so a model whose removal cascades or carries a `_postRemove` still goes through `remove<Model>(id)`. The
407
+ write-through pair is `pickAndWrite(id, data)` / `pickOneAndWrite(query, data)` — pick the document, write into it
408
+ and save it, so the save hooks do run; reach for those when the write has to be seen by them.
407
409
 
408
410
  **`<model>.service.ts`** — keep methods to a few lines: load → chain → `return await ….save()`. Write `return await`
409
411
  explicitly in tail position; do not "optimize" it away. Side effects belong in `override async _preUpdate` /
@@ -470,7 +472,7 @@ Full contract — credential handshake, room revalidation, socket cleanup scopin
470
472
  - **The guards are also the MCP exposure decision** — see MCP Exposure. An endpoint that names none is not published to agents at all, and a mutation whose only guard is `Public` is refused, so a missing `guards` array now costs visibility as well as authorization.
471
473
  - Resource guards are `Can<Verb><Model>` classes in `srvkit/guards.ts` that `implements Guard` with an `async canPass(context)`. They **fail closed**: no resource named ⇒ `false`; a load that throws ⇒ `logger.warn` then `false`. Admin bypass goes first.
472
474
  - Keep `static name = "User";` on guard classes. `fetch` serializes guard names and the API explorer filters on them; it looks like dead code, and deleting it breaks the UI. Comment it so the next reader knows.
473
- - **Every guard class also declares `static scope: GuardScope`, and it is required with no default.** `"account"` means the verdict reads the caller and nothing about the call, so it can be evaluated with no arguments — which is what lets an MCP listing hide what this caller certainly cannot use. `"resource"` means it needs the call's arguments (`context.getArg()`) and fails closed without them, so it is never evaluated for a listing: the entry stays visible and is stopped at call time. Getting it wrong is not a type error, so the marker is mandatory rather than defaulted — `SignedIn` / `Admin` / role checks are `"account"`, and every `Can<Verb><Model>` is `"resource"`.
475
+ - **Every guard class also declares `static scope: GuardScope`, and it is required with no default.** `"account"` means the verdict reads the caller and nothing about the call, so it can be evaluated with no arguments — which is what lets an MCP listing hide what this caller certainly cannot use. `"resource"` means it needs the call's arguments (`context.getArg()`) and fails closed without them, so it is never evaluated for a listing: the entry stays visible and is stopped at call time. Getting it wrong is not a type error, so the marker is mandatory rather than defaulted — `SignedIn` / `Admin` / role checks are `"account"`, and every `Can<Verb><Model>` is `"resource"`. An `"account"` guard that throws at listing time reached for arguments it does not have: the entry is hidden from every caller and the guard is named once in the log as mismarked. A guard that admits no model at all — `Person` — adds `static agents = false`; the MCP catalogue then refuses every endpoint it guards outright, so the person-only act is absent from the document rather than hidden per caller.
474
476
  - The acting user arrives via `.with(Self)` / `.with(CurrentUserId)` / `.with(Me)`. Never trust a client-supplied id.
475
477
  - Guards ship with the library that owns the model and are imported by its own signals through the package path, so a mounting app inherits authorization and cannot forget it.
476
478
  - Services re-check ownership even when a guard already gated the call — two independent gates.
@@ -515,7 +517,12 @@ Full contract — filter-arg `ref` pickers, `getQueryMeta` summary counters, `la
515
517
  `[object Object]`. It is an admin API: `root:` is always `Admin`.
516
518
  - A slice's `exec` returns a `QueryOf` (an opaque query descriptor); you **cannot** chain `.sort()`/`.limit()` on
517
519
  it. Apply ordering/paging via the store `init` fetch option instead: `initX(..., { sort, page, limit })`. For a
518
- chainable builder use the model facade's `findMany`/`findOne`.
520
+ chainable builder use the model facade's `find`/`findOne`.
521
+ - **A projection is bare on the facade and nested under `select` on a filter accessor.** `pickById(id, { secret:
522
+ true })` and `find`/`findOne`/`findById`/`pickOne` take the projection as their second argument; a generated
523
+ `findBy<Filter>(...args, { select: { secret: true } })` takes it inside the option object. The shapes do not swap
524
+ — `{ select: … }` handed to the facade projects a field named `select`, which no model has, and the read comes
525
+ back empty with no error. This is the only way to read a `field.secret(...)` value.
519
526
  - **A slice list is either one window or an accumulated one.** `setPageOf<Model>` swaps the window;
520
527
  `loadMoreOf<Model>()` appends the rows after the ones loaded and takes **no page number** — it skips by
521
528
  `<model>List.length`, so the offset cannot drift from what is on screen when a live insertion moves the server's
@@ -598,6 +605,7 @@ Conventions that hold for both shapes:
598
605
  - Best-effort code returns a sentinel (`null`, `undefined`, `[0, 0]`, `{}`). There are no Result/Either wrappers.
599
606
  - `try/catch` is rare and always converts an exception into a decision, never swallows one. Guards catch → `logger.warn` → `return false`; adapters catch → `logger.error` → `return null`; UI uses `try/finally` to reset a spinner. A bodyless `catch {}` is acceptable only with a one-line reason.
600
607
  - Store actions do not `try/catch` — let the framework toast the `Err`. Client-side validation failure is `msg.error("<key>")` plus an early return, never a throw.
608
+ - **A failure another process reported travels as itself.** A server-to-server `fetch.<endpoint>(..., { origin })` restores the remote `Err` — the same class, key and `data` — so a hop that rethrows it answers its own caller with that error and the dictionary translates it. Never wrap the catch in a `new Error`, and never re-key it as a `new Err` of your own: both discard what the endpoint chose to say, and a bare `Error` is generalized to `Internal Server Error` on the way out.
601
609
 
602
610
  ### MCP Exposure
603
611
 
@@ -616,16 +624,41 @@ it.
616
624
  slice and generated CRUD only, never a named slice or a custom endpoint. It is an opt-**out**, and it is
617
625
  curation, not authorization: HTTP serves the endpoint exactly as before.
618
626
  - **Narrow by cost, and read the boot log first.** MCP forbids a `$ref` across entries, so every entry inlines the
619
- full schema of every model it mentions and the listing is re-sent whole to every agent that connects. A plain
620
- 21-field model costs 12.6KB over eight entries; `mcp: { cru: false }` takes it to 4.7KB. `MCP catalogue: … ·
621
- listing 214KB` plus a per-signal `MCP catalogue cost:` line says where it went.
627
+ schema of every model it mentions and the listing is re-sent whole to every agent that connects. By default a
628
+ response schema names a nested model instead of inlining it (`outputSchema: "shallow"`; `"full"` restores the
629
+ closure, `"none"` drops the schema and keeps the text block) and a request schema asks for a relation's id, which
630
+ is what the wire carries. `MCP catalogue: … · listing 214KB` plus a per-signal `MCP catalogue cost:` line says
631
+ where the rest went; `mcp: { cru: false }` on a model is the next lever. `outputSchema` moves wire bytes and client
632
+ memory only — the model receives names, descriptions and input schemas (measured: 219 tools ≈ 41k tokens either
633
+ way) — so tool count and the input model behind `create*` / `update*` are what cost context. In one line:
634
+ `Person` for an act reserved for a person, `mcp: { cru: false }` for the tools whose input models cost the model
635
+ tokens, `outputSchema` for nothing that reaches the model.
622
636
  - Settings live in the app's `lib/option.ts` — `option.setMcp({ enabled, readOnly, path, version, instructions,
623
- allowedOrigins, pageSize, language, auth })`, **not `main.ts`**. Each field has an `AKAN_MCP_*` env spelling.
624
- `AKAN_MCP_READONLY=true` is the read-only-deployment valve, not the exposure switch; `AKAN_MCP=false` takes the
625
- whole surface off.
637
+ allowedOrigins, pageSize, language, outputSchema, rateLimit, auth })`, or `setMcp((env) => …)` for one derived from the server env, **not
638
+ `main.ts`**. Each field has an `AKAN_MCP_*` env spelling. `AKAN_MCP_READONLY=true` is the read-only-deployment
639
+ valve, not the exposure switch; `AKAN_MCP=false` takes the whole surface off. Every call that executes an
640
+ endpoint is rate-limited per caller (120 a minute, 8 in flight, per process; 429 + `Retry-After`) — `rateLimit`
641
+ / `AKAN_MCP_RATE_LIMIT` tunes or disables it.
642
+ - **Authentication is the same server.** An app mounting `libs/shared` serves the OAuth 2.1 authorization server
643
+ beside `/mcp` — `/.well-known/oauth-authorization-server`, `/oauth/authorize`, `/oauth/token`, `/oauth/register`
644
+ (RFC 7591; Cursor's `cursor://` redirect admitted) plus the consent page `libs/shared/page/oauth/consent` — and
645
+ names itself as the issuer, so `/mcp` demands a bearer token, verifies its signature, and refuses one carrying no
646
+ `aud`. A token is the app's own access JWT plus `aud`/`iss`/`client_id`, so `AccountMiddleware` and the guards
647
+ judge it unchanged; there is no scope. Per-app knobs live in `env.server.*` under `oauth` (`consentPath` and
648
+ `signinPath` with the basePath, `clients`, `dynamicRegistration`, `allowedRedirectSchemes`, `clientIdMetadata`,
649
+ `enabled`). `/mcp` reads the `Authorization` header only — a cookie is dropped at the door — and `JWT_SECRET` is
650
+ mandatory outside `local`, because the derived fallback is forgeable from three public names. A grant is revoked
651
+ whole — `POST /oauth/revoke` (RFC 7009) by the client, `fetch.revokeOAuthConnection(sessionId)` by the account's
652
+ owner, `fetch.listOAuthConnections()` to see them — and a revoked access token is refused at its next call.
653
+ - **A person may, a model may not.** An agent's token names its `client_id` and `aud`; a browser session has
654
+ neither, and `context.origin` is `"mcp"` for a call through the MCP endpoint. `guards: [Every, Person]` refuses a
655
+ model and, because `Person` declares `static agents = false`, takes the entry out of the MCP catalogue document
656
+ itself (named in the boot log like `mcp: false`); `.with(AgentCall)` hands an endpoint the same verdict as a
657
+ boolean to narrow what the call sets in motion; `isAgentCall(context)` is the function behind both
658
+ (`@libs/shared/srvkit`). Never sniff `aud` through a cast.
626
659
  - **The refusals are fail-closed**: a declared `mcp: false`, an endpoint with no `guards`, a mutation with no real
627
660
  guard, `pubsub` and `message`, an `Any` or `Upload` return, a file upload, a required `Any` argument, and the
628
- generated `light<Model>` read. A `prompt` also refuses a list argument and any `Any` argument. **Every refusal
661
+ generated `light<Model>` read. An `enumOf` argument is checked against its values on every path. **Every refusal
629
662
  is named in the boot log**, along with every published entry missing a dictionary `.desc()` — an agent picks a
630
663
  tool by its description.
631
664
  - A refused endpoint answers the *same* "unknown tool" as one that does not exist, and a guard's refusal is
@@ -633,10 +666,14 @@ it.
633
666
  - **A `field.visual` field is stripped from every MCP result** and from the readable schema so the two agree.
634
667
  - **A structured result ships twice by default** — as `structuredContent` and as the same JSON in the text block.
635
668
  `option.setMcp({ legacyTextBlock: false })` leaves a pointer there instead.
636
- - **`prompt()`** is invoked by the *user* as a slash command, never chosen by the model. `exec` returns
637
- `PromptMessage[]` built with `Msg.user` / `Msg.assistant` / `Msg.resource` / `Msg.image`; an embedded payload is
638
- masked by the model you name, and one whose `hidden`/`secret` fields are populated with no model named is
639
- refused. It is also mounted as a plain HTTP `GET` whether or not MCP is enabled, so guard it like any other read.
669
+ - **A prompt is a screen: `page().prompt(name, description)`.** There is no `prompt()` on `endpoint()`.
670
+ `prompts/get` runs the page's body in the RSC worker under the caller's token — nothing is rendered — and answers
671
+ the description, one `resource` per `fetch.*` the page made (masked by the endpoint's return model, addressed by
672
+ its `akan://` uri, one document once whatever shapes read it) and the published tools of the modules it fetched
673
+ from, minus what is already attached; lists — only lists — are cut to `promptBudget`. A guard refusing a query
674
+ inside the body reads as the screen refusing the account, the same way a redirect does. A
675
+ required argument left out answers a pointer to the `<model>List…` tool, never a guess. The description is the
676
+ only instruction, in English — `<Agent.Guide>` never reaches MCP, and the in-page chat lists no page prompts.
640
677
 
641
678
  Full contract — configuration, wire behaviour, resource URIs, OAuth metadata, protocol revisions, and
642
679
  `McpProgress.report`: `get_guideline` with `mcpRule`, or `akan guideline show mcpRule`.
@@ -659,7 +696,9 @@ what every developer must know even when not building one.
659
696
  every tool runs in the caller's own browser session, gated by guards and the approval card. Its guard is
660
697
  `AgentRelayAccess`, which refuses every call until the app names a guard of its own —
661
698
  `option.setAgentAccess(SignedIn)`. **The LLM is configured in `option.ts`, never through the environment**
662
- (`option.setLlm({ apiKey, model, host })`).
699
+ (`option.setLlm({ apiKey, model, host })`). Three providers ship — `DeepseekLlm` (the default, text only),
700
+ `OpenaiLlm` and `AnthropicLlm` (both read images) — and swapping one in is
701
+ `option.applyAdaptor(LlmAdaptorRole, AnthropicLlm)`.
663
702
  - **Declare the tool beside the control that already does it**, never as a separate surface:
664
703
  `st.tool("x").desc("…").arg("id", ID).exec(fn)` returns the callable to hand to `onClick` — one handler for the
665
704
  person and the agent. Publish it only where the screen already renders the control; a falsy name declares the
@@ -745,10 +784,11 @@ shape, so `cascade` never means "related" — it means one of exactly these:
745
784
  forms: a relation, an id with `ref`, or a polymorphic id with `refPath`. An array, a Map, `ref` together with
746
785
  `refPath`, and a field naming no owner each fail the class build.
747
786
  - **A `refPath` must name an `enumOf` field** — a free-form owner type is unknowable at build time. The one
748
- exception is opt-in and priced: `polymorphic: "any"` alongside `cascade: "removeWith"` takes a free-form
749
- `String` type field holding the owner's refName, and sweeps for children on **every** model's removal. The
750
- sweep is one indexed probe, but a single wildcard edge turns every cascade in the app back to one document at
751
- a time; the boot log names the edges in one `info` line.
787
+ exception is opt-in and priced: `cascade: "removeWithAny"` takes a free-form `String` type field holding the
788
+ owner's refName, and sweeps for children on **every** model's removal. The sweep is one indexed probe, but a
789
+ single wildcard edge turns every cascade in the app back to one document at a time; the boot log names the
790
+ edges in one `info` line. The action carries its own `refPath` in the option type, so the widening cannot be
791
+ declared apart from the direction it widens.
752
792
  - **A cascade goes through the target's service, never its model** — that path is what runs the target's
753
793
  `_postRemove`, where a module puts the side effect the removal has to carry.
754
794
  - **Nothing checks whether another document still references the same target.** `File` in particular is deduped by
@@ -767,39 +807,50 @@ shape, so `cascade` never means "related" — it means one of exactly these:
767
807
  - Reserved `_*.tsx` route filenames are limited to `_index.tsx`, `_layout.tsx`, and `_overrides.tsx`; do not add files like `_Component.tsx` or `_helper.tsx`.
768
808
  - Page filenames must not start with an uppercase letter. Move helper components like `Component.tsx` to app `ui`, `common`, or `lib` instead.
769
809
  - Dynamic segments use `[id]`; route groups use directories like `(user)`, `(public)`, `(tab)`, or `(detail)`.
770
- - Page modules should usually export `default`, `pageConfig`, `head`, `generateHead`, or `Loading`.
810
+ - **A route file exports one thing**: `export default page()…render(…)` for a page, `layout()…render(…)` for a
811
+ `_layout.tsx`, `rootLayout()…render(…)` for the app's (or a basePath's) root `_layout.tsx`. Every route setting
812
+ is a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()` / `.metadata()`, `.loading()`;
813
+ `.notFound()` / `.error()` on a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()`
814
+ / `.layoutStyle()` / `.gaTrackingId()` on the root layout; `.prompt()` on a page. A named export beside the chain,
815
+ or `page()` in a `_layout.tsx`, fails the build.
816
+ - **A page names every `[x]` segment of its path with `.param("x", Type)`** and reads a query key only through
817
+ `.search("k", Type)`. Values arrive typed — `ID`/`String` → string, `Int`/`Float` → number, `Boolean`, `Date` →
818
+ Dayjs, an `enumOf` class → its union, `[T]` → array — a path value the type refuses answers not-found, and a
819
+ search value that fails is dropped. A layout may declare a subset. Names are string literals: `akan sync` reads
820
+ them off the source and refuses a `[projectId]` folder whose page declares no `.param("projectId")`. `lang` is
821
+ never declared: every route sits under `/:lang`, and the locale segment reaches every stage as `lang`.
771
822
  - `_overrides.tsx` is a logic-free UI-override manifest: imports plus a single `export default override({ Slot: AppComponent })` (from `akanjs/ui`), no `"use client"`. It re-skins framework `akanjs/ui` components for its route subtree; nested manifests merge over ancestors slot-by-slot (closest wins). See the UI Customization reference for the slot list.
772
- - Prefer `export default function Page` or `export default async function Page` for page components.
823
+ - The legacy shape — `export default async function Page` beside `pageConfig` / `head` / `Loading` exports — still
824
+ loads and logs one deprecation warning per file at boot. Migrate with the `workspaceRecipes` recipe "Migrating
825
+ `page/` to the route chain"; new files are never written in it.
773
826
  - `libs/<lib>/page` follows the same rules and ships routes to apps that opt in with `syncPageLibs` in `akan.config.ts`: `true` takes every lib dep that has a `page` folder, an array takes the libs listed, `false` (the default) syncs nothing.
774
827
  - `akan sync` links those routes into `apps/<app>/page/(libs)/(<lib>)` — once per basePath when the app declares subRoutes. The folder is generated and gitignored; edit the lib source, never the link.
775
828
  - Both path segments are route groups, so a lib route mounts at its own path (`libs/<lib>/page/login/_index.tsx` serves `/login`). Two synced routes that resolve to the same pattern are a sync-time error.
776
- - `export const pageConfig = { devOnly: true }` keeps a route out of `akan build` while it keeps serving under `akan start` and keeps being typechecked. On a `_layout.tsx` it excludes every route under that directory too. Write it as a literal `true`/`false` — the build reads it off the source without evaluating the module.
829
+ - `.config({ devOnly: true })` keeps a route out of `akan build` while it keeps serving under `akan start` and keeps being typechecked. On a `_layout.tsx` it excludes every route under that directory too. Write it as a literal `true`/`false` — the build reads it off the source without evaluating the module.
777
830
  - Before changing route behavior, check `pkgs/akanjs/server/routeTreeBuilder.ts` and nearby routes for the expected pattern.
778
831
 
779
832
  ### Page Body Shape
780
833
 
781
834
  ```tsx
782
- interface PageProps {
783
- params: { orgId: string };
784
- }
785
-
786
- export default async function Page({ params }: PageProps) {
787
- const { l } = usePage();
788
- getSelf({ unauthorize: "/signin" });
789
- const { orgId } = params;
790
- const [{ org }, { taskInitInOrg }] = await Promise.all([fetch.viewOrg(orgId), fetch.initTaskInOrg(orgId)]);
791
- return <Task.Zone.Card init={taskInitInOrg} prefix={`/org/${orgId}`} />;
792
- }
793
-
794
- export const pageConfig = { transition: "stack" } satisfies PageConfig;
835
+ export default page()
836
+ .param("orgId", ID)
837
+ .config({ transition: "stack" })
838
+ .render(async ({ orgId }) => {
839
+ const { l } = usePage();
840
+ getSelf({ unauthorize: "/signin" });
841
+ const [{ org }, { taskInitInOrg }] = await Promise.all([fetch.viewOrg(orgId), fetch.initTaskInOrg(orgId)]);
842
+ return <Task.Zone.Card init={taskInitInOrg} prefix={`/org/${orgId}`} />;
843
+ });
795
844
  ```
796
845
 
797
- - There is no `loader=` / `render=` page prop. Pages are `export default async function Page`.
798
- - Declare `interface PageProps { params: {...}; searchParams?: {...} }` immediately above the default export.
799
- - Body order: `usePage()`, auth, destructure params, fetch, return.
846
+ - There is no `loader=` / `render=` page prop and no `PageProps` interface: the chain declares the arguments and
847
+ `.render()` receives them flat, typed, with `lang` on every route and `children` added for a layout.
848
+ - Body order inside `.render()`: `usePage()`, auth, fetch, return.
800
849
  - Run independent fetches through `Promise.all`, even when there is only one.
801
850
  - Gate auth at `_layout.tsx`; repeating `getSelf({ unauthorize: "/signin" })` in the page is fine and common.
802
- - Keep `async` even when nothing is awaited — it marks a real server page.
851
+ - `async` on the render callback only when the body awaits; the callback is not a React component, so it takes no
852
+ name and no return type.
853
+ - Publish a screen to agents with `.prompt(name, description)` — see MCP Exposure.
803
854
  - No `useState`, no `useEffect`, and no comments in page files.
804
855
 
805
856
  ## Akan Sync Conventions (`apps/**`, `libs/**`)
@@ -872,9 +923,18 @@ when two shapes disagree.
872
923
  one — `host` for the dev host, gateway and RSC worker, plus a row per replica the gateway forwards — and the
873
924
  selected one's log beside it. `Tab` walks every row and `1`-`9` jump to that app (the rail shows each
874
925
  app's digit), `↑↓` scroll (shift for a page, `G` back to following),
875
- `/` greps, `e` shows stderr only, `c` clears, `o` opens the browser, `r` restarts that app, `q` quits.
926
+ `/` greps, `e` shows stderr only, `c` clears, `y` copies, `Y` copies the log path, `o` opens the browser,
927
+ `r` restarts that app, `q` quits.
876
928
  **`--plain` prints prefixed interleaved lines instead**, and a pipe, a redirect or an unsized terminal
877
929
  downgrades to that on its own.
930
+ - **A supervised session writes `local/apps/<app>/runtime/dev.log`, and that path is how a log gets handed over.**
931
+ Ink repaints one frame in place, so the full-screen view leaves a bordered screenshot in the scrollback and
932
+ takes the rest of the session with it — a drag-selection is cleared by the next repaint, and what survives
933
+ carries borders and lines truncated to the pane. The file is the same output with no ANSI, nothing truncated,
934
+ and every process of the app in arrival order, including the dev host's own build output, which reaches no
935
+ other file. The previous session is kept beside it as `dev.prev.log`. Hand somebody — or an agent — the
936
+ path rather than a paste: it costs one line instead of a transcript, and it can be re-read after a fix.
937
+ In the view, `y` copies the lines the filters have already narrowed and `Y` copies the path.
878
938
  - **How many apps boot at once is the machine's answer, not a constant.** A cold boot build is the builder's
879
939
  RSS peak (~900MB per app), so the default wave is what memory and cores allow — half the memory budget
880
940
  divided by that peak, and one app per four cores — which boots a laptop's apps together and still staggers
@@ -14,7 +14,17 @@
14
14
  ],
15
15
  "update": {
16
16
  "filePath": "./mcpRule.instruction.md",
17
- "contents": ["exposure rule", "configuration", "declaring endpoints", "refusals and wire behaviour", "prompts"],
17
+ "contents": [
18
+ "exposure rule",
19
+ "configuration",
20
+ "declaring endpoints",
21
+ "refusals and wire behaviour",
22
+ "prompts",
23
+ "rolling out to an app",
24
+ "choosing what to open",
25
+ "writing instructions",
26
+ "verifying an app"
27
+ ],
18
28
  "rules": [
19
29
  "Use current Akan APIs only",
20
30
  "State the rule before the reason, and keep the reason to one clause",