@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.
- package/.build-stamp +1 -1
- package/{abstractCompactor-byytxs22.js → abstractCompactor-mjh5ppsq.js} +1 -1
- package/{agent.command-v1a4746v.js → agent.command-d2gfxfck.js} +5 -5
- package/{application.command-j05fr5jc.js → application.command-pj2m7584.js} +5 -5
- package/{applicationBuildRunner-s4gsk728.js → applicationBuildRunner-kxwbssgn.js} +2 -2
- package/{applicationReleasePackager-28v9vbzk.js → applicationReleasePackager-4yasxwv9.js} +1 -1
- package/buildBatch.proc.js +2 -2
- package/{capacitorApp-8d6d4zkm.js → capacitorApp-36rbr858.js} +2 -2
- package/{cloud.command-r6wvq2rp.js → cloud.command-jshwdq22.js} +9 -9
- package/{context.command-4vdt1aqq.js → context.command-ez8a0zyz.js} +13 -13
- package/{devTui-tnj2hh4f.js → devTui-40ggmd99.js} +85 -84
- package/{devTuiView-xzed2pd6.js → devTuiView-c08ccbat.js} +1 -1
- package/{guideline.command-p92dq6ts.js → guideline.command-xmqwx8zm.js} +11 -7
- package/guidelines/agentRule/agentRule.instruction.md +112 -32
- package/guidelines/conventions/conventions.instruction.md +100 -40
- package/guidelines/mcpRule/mcpRule.generate.json +11 -1
- package/guidelines/mcpRule/mcpRule.instruction.md +296 -53
- package/guidelines/queryRule/queryRule.instruction.md +17 -9
- package/guidelines/ssrRule/ssrRule.instruction.md +15 -13
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +10 -5
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +68 -3
- package/incrementalBuilder.proc.js +1 -1
- package/{index-gq44bqjt.js → index-259dsnz3.js} +2 -2
- package/{index-b9rkcrx8.js → index-2hq52jq3.js} +9 -7
- package/{index-qsq3q8fy.js → index-2tv3fdx4.js} +21 -10
- package/{index-qkk27bk6.js → index-4edzf1gr.js} +4 -4
- package/{index-yza7zkpt.js → index-600zhnve.js} +10 -10
- package/{index-srdssqj7.js → index-7737gxcp.js} +3 -3
- package/{index-zmjppp25.js → index-8pm30m73.js} +5 -5
- package/{index-p5neav9b.js → index-a64esmtb.js} +54 -9
- package/{index-0amdxycf.js → index-fz5q3gd7.js} +3 -3
- package/{index-fc1bh0a7.js → index-gyd220p2.js} +1 -1
- package/{index-5svjba73.js → index-j9yhg404.js} +1 -1
- package/{index-c69j78kk.js → index-kn1h7p1x.js} +1 -1
- package/{index-gtmz74tj.js → index-ktb5njgy.js} +2 -2
- package/{index-znynzt5w.js → index-mak3cd28.js} +1 -1
- package/{index-29sxqfaw.js → index-ne2qy0nk.js} +3 -3
- package/{index-hyywke0f.js → index-pnqbkf5p.js} +1 -1
- package/{index-8p8p0cf8.js → index-rwg2aa4f.js} +3 -3
- package/{index-5wd728df.js → index-v0qh106m.js} +215 -12
- package/{index-7d6vny25.js → index-ws5e0j51.js} +2 -2
- package/{index-1yxtbmbj.js → index-wt7qrtd4.js} +1 -1
- package/{index-gkkdx9kp.js → index-zry5nz81.js} +6 -6
- package/index.js +19 -19
- package/{library.command-s4fszngs.js → library.command-nbsky672.js} +3 -3
- package/{localRegistry.command-fq2w7cgz.js → localRegistry.command-yeq57wjr.js} +8 -8
- package/{module.command-6n7dwvav.js → module.command-29vqzrh8.js} +6 -6
- package/{package.command-gr82ypnr.js → package.command-axv0abmn.js} +3 -3
- package/package.json +2 -2
- package/{page.command-7gvgb3jb.js → page.command-vs9s3zrh.js} +3 -3
- package/{primitive.command-tk5ckw38.js → primitive.command-cbxmqbtc.js} +7 -7
- package/{quality.command-38v81baf.js → quality.command-gmbs7nws.js} +2 -2
- package/{repair.command-5xbwvsxp.js → repair.command-bqsjptsj.js} +5 -5
- package/{routeSourceValidator-ze6x894c.js → routeSourceValidator-apyqw7wk.js} +90 -17
- package/{scalar.command-0epwcr5k.js → scalar.command-6k1vhe9s.js} +5 -5
- package/{subspace.command-e2t36xb2.js → subspace.command-fwjfcts7.js} +7 -5
- package/templates/app/page/_index.tsx +3 -3
- package/templates/app/page/_layout.tsx +31 -32
- package/templates/appSample/page/task/[taskId]/_index.tsx +22 -27
- package/templates/appSample/page/task/[taskId]/edit.tsx +25 -30
- package/templates/appSample/page/task/_index.tsx +3 -7
- package/templates/appSample/page/task/_layout.tsx +4 -15
- package/templates/appSample/page/task/new.tsx +4 -10
- package/templates/crudPages/[__model__Id]/edit/page.tsx +26 -28
- package/templates/crudPages/[__model__Id]/page.tsx +20 -25
- package/templates/crudPages/new/page.tsx +24 -23
- package/templates/crudPages/page.tsx +25 -24
- package/templates/crudSinglePage/page.tsx +25 -24
- package/templates/lib/sig.ts +4 -0
- package/{workflow.command-kphbr7eh.js → workflow.command-nwpcdc8p.js} +10 -10
- 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,
|
|
76
|
-
|
|
77
|
-
`onDelta`
|
|
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.
|
|
97
|
-
|
|
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. **
|
|
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
|
-
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
is the
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
is worth waiting for — publish an `st.tool` beside the
|
|
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
|
|
455
|
-
describes the moment before the change landed and the `readScreen` that follows reads the
|
|
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 `
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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 })`,
|
|
624
|
-
`AKAN_MCP_READONLY=true` is the read-only-deployment
|
|
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.
|
|
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
|
-
-
|
|
637
|
-
`
|
|
638
|
-
|
|
639
|
-
|
|
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: `
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
|
798
|
-
|
|
799
|
-
- Body order
|
|
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
|
-
-
|
|
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, `
|
|
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": [
|
|
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",
|