@akanjs/cli 3.0.0-alpha.103 → 3.0.0-alpha.105

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 (68) 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-fzdcnzhg.js → application.command-xgphqs7k.js} +5 -5
  5. package/{applicationBuildRunner-s4gsk728.js → applicationBuildRunner-3d5q1yn7.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-wygy2z05.js → cloud.command-abgkk73f.js} +9 -9
  10. package/{context.command-4vdt1aqq.js → context.command-ez8a0zyz.js} +13 -13
  11. package/{devTui-qnf1txdz.js → devTui-70cjjtaw.js} +1 -1
  12. package/{devTuiView-yzct2cq6.js → devTuiView-71wnkjft.js} +1 -1
  13. package/{guideline.command-p92dq6ts.js → guideline.command-xmqwx8zm.js} +11 -7
  14. package/guidelines/agentRule/agentRule.instruction.md +6 -14
  15. package/guidelines/conventions/conventions.instruction.md +40 -25
  16. package/guidelines/mcpRule/mcpRule.instruction.md +61 -39
  17. package/guidelines/ssrRule/ssrRule.instruction.md +15 -13
  18. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -3
  19. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +68 -3
  20. package/incrementalBuilder.proc.js +1 -1
  21. package/{index-p5neav9b.js → index-0vh9rgxz.js} +1 -1
  22. package/{index-gq44bqjt.js → index-259dsnz3.js} +2 -2
  23. package/{index-qsq3q8fy.js → index-2tv3fdx4.js} +21 -10
  24. package/{index-yks023mb.js → index-4c9ykbfq.js} +6 -6
  25. package/{index-qkk27bk6.js → index-4edzf1gr.js} +4 -4
  26. package/{index-yza7zkpt.js → index-600zhnve.js} +10 -10
  27. package/{index-srdssqj7.js → index-7737gxcp.js} +3 -3
  28. package/{index-zmjppp25.js → index-8pm30m73.js} +5 -5
  29. package/{index-0amdxycf.js → index-fz5q3gd7.js} +3 -3
  30. package/{index-fc1bh0a7.js → index-gyd220p2.js} +1 -1
  31. package/{index-5svjba73.js → index-j9yhg404.js} +1 -1
  32. package/{index-c69j78kk.js → index-kn1h7p1x.js} +1 -1
  33. package/{index-gtmz74tj.js → index-ktb5njgy.js} +2 -2
  34. package/{index-znynzt5w.js → index-mak3cd28.js} +1 -1
  35. package/{index-29sxqfaw.js → index-ne2qy0nk.js} +3 -3
  36. package/{index-hyywke0f.js → index-pnqbkf5p.js} +1 -1
  37. package/{index-8p8p0cf8.js → index-rwg2aa4f.js} +3 -3
  38. package/{index-7d6vny25.js → index-ws5e0j51.js} +2 -2
  39. package/{index-1yxtbmbj.js → index-wt7qrtd4.js} +1 -1
  40. package/{index-zz5jyd94.js → index-yecdrt9a.js} +9 -9
  41. package/{index-zxgr3s8s.js → index-yw4xj7pa.js} +4 -4
  42. package/index.js +19 -19
  43. package/{library.command-s4fszngs.js → library.command-nbsky672.js} +3 -3
  44. package/{localRegistry.command-gyw4da9z.js → localRegistry.command-namg61k1.js} +8 -8
  45. package/{module.command-6n7dwvav.js → module.command-29vqzrh8.js} +6 -6
  46. package/{package.command-gr82ypnr.js → package.command-axv0abmn.js} +3 -3
  47. package/package.json +2 -2
  48. package/{page.command-7gvgb3jb.js → page.command-vs9s3zrh.js} +3 -3
  49. package/{primitive.command-tk5ckw38.js → primitive.command-cbxmqbtc.js} +7 -7
  50. package/{quality.command-38v81baf.js → quality.command-gmbs7nws.js} +2 -2
  51. package/{repair.command-5xbwvsxp.js → repair.command-bqsjptsj.js} +5 -5
  52. package/{routeSourceValidator-ze6x894c.js → routeSourceValidator-apyqw7wk.js} +90 -17
  53. package/{scalar.command-0epwcr5k.js → scalar.command-6k1vhe9s.js} +5 -5
  54. package/{subspace.command-e2t36xb2.js → subspace.command-35a3t6vv.js} +3 -3
  55. package/templates/app/page/_index.tsx +3 -3
  56. package/templates/app/page/_layout.tsx +31 -32
  57. package/templates/appSample/page/task/[taskId]/_index.tsx +22 -27
  58. package/templates/appSample/page/task/[taskId]/edit.tsx +25 -30
  59. package/templates/appSample/page/task/_index.tsx +3 -7
  60. package/templates/appSample/page/task/_layout.tsx +4 -15
  61. package/templates/appSample/page/task/new.tsx +4 -10
  62. package/templates/crudPages/[__model__Id]/edit/page.tsx +26 -28
  63. package/templates/crudPages/[__model__Id]/page.tsx +20 -25
  64. package/templates/crudPages/new/page.tsx +24 -23
  65. package/templates/crudPages/page.tsx +25 -24
  66. package/templates/crudSinglePage/page.tsx +25 -24
  67. package/{workflow.command-kphbr7eh.js → workflow.command-nwpcdc8p.js} +10 -10
  68. package/{workspace.command-j1gatz3d.js → workspace.command-kah5jgg5.js} +20 -20
package/.build-stamp CHANGED
@@ -1 +1 @@
1
- 708d95c2692df8b7a0bdb905802d48d38352e0305215030af6c3d6aa468a9460
1
+ 9fac8ac9683bf6e7c0ba77ac71e733d1a3e440dcfdf417462aefd61e86a95904
@@ -2,7 +2,7 @@
2
2
  import"./index-q0y2jq9j.js";
3
3
  import {
4
4
  AiSession
5
- } from "./index-c69j78kk.js";
5
+ } from "./index-kn1h7p1x.js";
6
6
  import {
7
7
  AbstractDoc
8
8
  } from "./index-meh558jy.js";
@@ -2,17 +2,17 @@
2
2
  import {
3
3
  Workspace,
4
4
  command
5
- } from "./index-znynzt5w.js";
5
+ } from "./index-mak3cd28.js";
6
6
  import"./index-q0y2jq9j.js";
7
- import"./index-qsq3q8fy.js";
7
+ import"./index-2tv3fdx4.js";
8
8
  import"./index-6wwzs1bc.js";
9
9
  import"./index-by5gpp8x.js";
10
- import"./index-fc1bh0a7.js";
11
- import"./index-gtmz74tj.js";
10
+ import"./index-gyd220p2.js";
11
+ import"./index-ktb5njgy.js";
12
12
  import"./index-h7yw2z9n.js";
13
13
  import {
14
14
  AgentScript
15
- } from "./index-29sxqfaw.js";
15
+ } from "./index-ne2qy0nk.js";
16
16
 
17
17
  // pkgs/@akanjs/cli/agent/agent.command.ts
18
18
  class AgentCommand extends command("agent", [AgentScript], ({ public: target }) => ({
@@ -6,19 +6,19 @@ import {
6
6
  Exec,
7
7
  Workspace,
8
8
  command
9
- } from "./index-znynzt5w.js";
9
+ } from "./index-mak3cd28.js";
10
10
  import"./index-q0y2jq9j.js";
11
- import"./index-qsq3q8fy.js";
11
+ import"./index-2tv3fdx4.js";
12
12
  import"./index-6wwzs1bc.js";
13
13
  import {
14
14
  ApplicationScript
15
- } from "./index-zz5jyd94.js";
16
- import"./index-gq44bqjt.js";
15
+ } from "./index-yecdrt9a.js";
16
+ import"./index-259dsnz3.js";
17
17
  import"./index-hwkygn9s.js";
18
18
  import {
19
19
  AbstractDoc
20
20
  } from "./index-meh558jy.js";
21
- import"./index-5svjba73.js";
21
+ import"./index-j9yhg404.js";
22
22
  import {
23
23
  getMobileTargetChoices
24
24
  } from "./index-47pj3aee.js";
@@ -1,14 +1,14 @@
1
1
  // @bun
2
2
  import {
3
3
  Spinner
4
- } from "./index-qsq3q8fy.js";
4
+ } from "./index-2tv3fdx4.js";
5
5
  import {
6
6
  AllRoutesBuilder,
7
7
  CsrArtifactBuilder,
8
8
  FontPruner,
9
9
  precompressArtifacts,
10
10
  SsrBaseArtifactBuilder
11
- } from "./index-p5neav9b.js";
11
+ } from "./index-0vh9rgxz.js";
12
12
  import"./index-w8kmx1b6.js";
13
13
  import"./index-q0y2jq9j.js";
14
14
  import"./index-hwkygn9s.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./index-q0y2jq9j.js";
5
5
  import {
6
6
  Spinner
7
- } from "./index-qsq3q8fy.js";
7
+ } from "./index-2tv3fdx4.js";
8
8
 
9
9
  // pkgs/@akanjs/devkit/applicationReleasePackager.ts
10
10
  import { cp, mkdir, rm } from "fs/promises";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  WorkspaceExecutor,
4
4
  AppExecutor
5
- } from "./index-qsq3q8fy.js";
5
+ } from "./index-2tv3fdx4.js";
6
6
  import"./index-q0y2jq9j.js";
7
7
  import"./index-6wwzs1bc.js";
8
8
  import {
@@ -11,7 +11,7 @@ import {
11
11
  CsrArtifactBuilder,
12
12
  FontOptimizer,
13
13
  SsrBaseArtifactBuilder
14
- } from "./index-p5neav9b.js";
14
+ } from "./index-0vh9rgxz.js";
15
15
  import"./index-w8kmx1b6.js";
16
16
  import"./index-hwkygn9s.js";
17
17
  import"./index-n68jx79w.js";
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  isPlaceholderAppId2,
4
4
  CapacitorApp2
5
- } from "./index-1yxtbmbj.js";
6
- import"./index-qsq3q8fy.js";
5
+ } from "./index-wt7qrtd4.js";
6
+ import"./index-2tv3fdx4.js";
7
7
  import"./index-6wwzs1bc.js";
8
8
  import"./index-q0y2jq9j.js";
9
9
  export {
@@ -2,23 +2,23 @@
2
2
  import"./index-q0y2jq9j.js";
3
3
  import {
4
4
  GlobalConfig
5
- } from "./index-c69j78kk.js";
5
+ } from "./index-kn1h7p1x.js";
6
6
  import {
7
7
  Workspace,
8
8
  command
9
- } from "./index-znynzt5w.js";
10
- import"./index-qsq3q8fy.js";
9
+ } from "./index-mak3cd28.js";
10
+ import"./index-2tv3fdx4.js";
11
11
  import"./index-6wwzs1bc.js";
12
- import"./index-zz5jyd94.js";
13
- import"./index-gq44bqjt.js";
12
+ import"./index-yecdrt9a.js";
13
+ import"./index-259dsnz3.js";
14
14
  import"./index-hwkygn9s.js";
15
15
  import"./index-kcxmv48c.js";
16
- import"./index-7d6vny25.js";
17
- import"./index-zxgr3s8s.js";
16
+ import"./index-ws5e0j51.js";
17
+ import"./index-yw4xj7pa.js";
18
18
  import {
19
19
  CloudScript
20
- } from "./index-yks023mb.js";
21
- import"./index-5svjba73.js";
20
+ } from "./index-4c9ykbfq.js";
21
+ import"./index-j9yhg404.js";
22
22
 
23
23
  // pkgs/@akanjs/cli/cloud/cloud.command.ts
24
24
  var localRegistryUrl = () => process.env.AKAN_NPM_REGISTRY ?? "http://127.0.0.1:4873";
@@ -2,25 +2,25 @@
2
2
  import {
3
3
  Workspace,
4
4
  command
5
- } from "./index-znynzt5w.js";
5
+ } from "./index-mak3cd28.js";
6
6
  import"./index-q0y2jq9j.js";
7
- import"./index-qsq3q8fy.js";
7
+ import"./index-2tv3fdx4.js";
8
8
  import"./index-6wwzs1bc.js";
9
9
  import"./index-by5gpp8x.js";
10
- import"./index-fc1bh0a7.js";
11
- import"./index-gtmz74tj.js";
10
+ import"./index-gyd220p2.js";
11
+ import"./index-ktb5njgy.js";
12
12
  import {
13
13
  ContextScript
14
- } from "./index-yza7zkpt.js";
15
- import"./index-1yxtbmbj.js";
14
+ } from "./index-600zhnve.js";
15
+ import"./index-wt7qrtd4.js";
16
16
  import"./index-h7yw2z9n.js";
17
- import"./index-0amdxycf.js";
18
- import"./index-srdssqj7.js";
19
- import"./index-c69j78kk.js";
20
- import"./index-zmjppp25.js";
21
- import"./index-hyywke0f.js";
22
- import"./index-qkk27bk6.js";
23
- import"./index-8p8p0cf8.js";
17
+ import"./index-fz5q3gd7.js";
18
+ import"./index-7737gxcp.js";
19
+ import"./index-kn1h7p1x.js";
20
+ import"./index-8pm30m73.js";
21
+ import"./index-pnqbkf5p.js";
22
+ import"./index-4edzf1gr.js";
23
+ import"./index-rwg2aa4f.js";
24
24
 
25
25
  // pkgs/@akanjs/cli/context/context.command.ts
26
26
  class ContextCommand extends command("context", [ContextScript], ({ public: target }) => ({
@@ -4,7 +4,7 @@ import {
4
4
  plainTextOf,
5
5
  HOST_SOURCE,
6
6
  DevLogBuffer
7
- } from "./index-zz5jyd94.js";
7
+ } from "./index-yecdrt9a.js";
8
8
 
9
9
  // pkgs/@akanjs/cli/application/devTui.tsx
10
10
  import { render } from "ink";
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // pkgs/@akanjs/cli/application/devTuiView.ts
3
3
  var createDevTuiView = async (supervisor) => {
4
- const { DevTui } = await import("./devTui-qnf1txdz.js");
4
+ const { DevTui } = await import("./devTui-70cjjtaw.js");
5
5
  return new DevTui(supervisor);
6
6
  };
7
7
  export {
@@ -4,11 +4,11 @@ import {
4
4
  runner,
5
5
  script,
6
6
  command
7
- } from "./index-znynzt5w.js";
7
+ } from "./index-mak3cd28.js";
8
8
  import {
9
9
  FileSys
10
10
  } from "./index-q0y2jq9j.js";
11
- import"./index-qsq3q8fy.js";
11
+ import"./index-2tv3fdx4.js";
12
12
  import {
13
13
  getDirname2
14
14
  } from "./index-6wwzs1bc.js";
@@ -17,7 +17,7 @@ import {
17
17
  } from "./index-h7yw2z9n.js";
18
18
  import {
19
19
  AiSession
20
- } from "./index-c69j78kk.js";
20
+ } from "./index-kn1h7p1x.js";
21
21
 
22
22
  // pkgs/@akanjs/cli/guideline/guideline.script.ts
23
23
  import { Logger } from "akanjs/common";
@@ -135,9 +135,11 @@ Request: ${updateRequest}
135
135
  async requestCreateDocumentPage(page) {
136
136
  const writePath = this.#resolveDocsPagePath(page);
137
137
  if (!await this.workspace.exists(writePath))
138
- await this.workspace.writeFile(writePath, `export default function Page() {
138
+ await this.workspace.writeFile(writePath, `import { page } from "akanjs/client";
139
+
140
+ export default page().render(() => {
139
141
  return <div>No Content</div>;
140
- }
142
+ });
141
143
  `);
142
144
  const instruction = await Prompter.getInstruction(this.name);
143
145
  const pageContent = await this.workspace.getLocalFile(writePath);
@@ -178,9 +180,11 @@ Please return only the file result in the following format for easy parsing.
178
180
  async requestUpdateDocumentPage(page, updateRequest) {
179
181
  const writePath = this.#resolveDocsPagePath(page);
180
182
  if (!await this.workspace.exists(writePath))
181
- await this.workspace.writeFile(writePath, `export default function Page() {
183
+ await this.workspace.writeFile(writePath, `import { page } from "akanjs/client";
184
+
185
+ export default page().render(() => {
182
186
  return <div>No Content</div>;
183
- }
187
+ });
184
188
  `);
185
189
  const instruction = await Prompter.getInstruction(this.name);
186
190
  const pageContent = await this.workspace.getLocalFile(writePath);
@@ -93,8 +93,7 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
93
93
  model never saw is one it answers about from the filename. An adaptor declares `accepts: { image, document }` and
94
94
  `AgentService.readable` degrades the rest, so a text-only provider needs no attachment code at all — DeepSeek
95
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
96
+ works, `text` being readable by every model there is. Persisting keeps each
98
97
  attachment's name and drops its content: web storage is a few megabytes, one screenshot fills a chunk of it, and
99
98
  a save that fails is silent — so keeping the bytes would quietly stop keeping the transcript.
100
99
  **The ceilings are the message's, not the file's**: 4 MB per file, 8 MB and five files per message, and the same
@@ -372,18 +371,11 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
372
371
  useless to a model — that is cheaper than every tool learning to avoid it.
373
372
 
374
373
  ## Slash Commands And The Transcript
375
- - **`prompt()` endpoints double as the chat's slash commands.** There is no listing endpoint — the client reads
376
- its own serialized signals — so a prompt's dictionary `.desc()` is what the menu shows, and its guards are
377
- enforced by the prompt's own GET at call time. Arguments are positional and whitespace-separated, and quoting
378
- is how a sentence stays one of them (`/reviewTask t1 "look at the totals"`) — a prompt taking a single `String`
379
- is the common case, and an unquoted sentence would fill its second parameter with the second word.
380
- - **The chat answers six slash commands of its own**, listed in the same `/` menu ahead of the prompts:
381
- `/new` (`/clear`), `/retry`, `/compact`, `/copy`, `/help` and `/tools`. An app writes none of them and cannot add
382
- one — the extension point for a product's own command is a `prompt()` endpoint, which is guarded and server-side.
383
- **A built-in wins a name collision with a prompt of the same name**, the mirror image of the tool rule: a
384
- component's `st.tool` shadows a built-in it means to replace, but no library's prompt may take `/new` away from
385
- the user who typed it — so a shadowed prompt is dropped from the menu rather than listed twice. `/new` and
386
- `/copy` are also dispatched *before* the is-a-turn-running check **and before the question card takes the
374
+ - **The `/` menu is the chat's own.** An app's prompts are `page().prompt()` declarations served over MCP to
375
+ external agents; a browser chat lists none of them, so the menu holds the six built-in commands and nothing else.
376
+ - **The chat answers six slash commands of its own**: `/new` (`/clear`), `/retry`, `/compact`, `/copy`, `/help` and
377
+ `/tools`. An app writes none of them and cannot add one — a workflow a person invokes by name belongs to MCP, as
378
+ 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
387
379
  composer**, because mid-turn is exactly when they are reached for and a question the agent asked is the middle
388
380
  of a turn like any other — answered as text, `/new` would have reached the model as the user's decision.
389
381
  `/new` therefore aborts the turn it is clearing and waits for it to wind down, since the loop clears its own
@@ -651,7 +651,7 @@ it.
651
651
  (`@libs/shared/srvkit`). Never sniff `aud` through a cast.
652
652
  - **The refusals are fail-closed**: a declared `mcp: false`, an endpoint with no `guards`, a mutation with no real
653
653
  guard, `pubsub` and `message`, an `Any` or `Upload` return, a file upload, a required `Any` argument, and the
654
- generated `light<Model>` read. A `prompt` also refuses a list argument and any `Any` argument. **Every refusal
654
+ generated `light<Model>` read. An `enumOf` argument is checked against its values on every path. **Every refusal
655
655
  is named in the boot log**, along with every published entry missing a dictionary `.desc()` — an agent picks a
656
656
  tool by its description.
657
657
  - A refused endpoint answers the *same* "unknown tool" as one that does not exist, and a guard's refusal is
@@ -659,10 +659,14 @@ it.
659
659
  - **A `field.visual` field is stripped from every MCP result** and from the readable schema so the two agree.
660
660
  - **A structured result ships twice by default** — as `structuredContent` and as the same JSON in the text block.
661
661
  `option.setMcp({ legacyTextBlock: false })` leaves a pointer there instead.
662
- - **`prompt()`** is invoked by the *user* as a slash command, never chosen by the model. `exec` returns
663
- `PromptMessage[]` built with `Msg.user` / `Msg.assistant` / `Msg.resource` / `Msg.image`; an embedded payload is
664
- masked by the model you name, and one whose `hidden`/`secret` fields are populated with no model named is
665
- refused. It is also mounted as a plain HTTP `GET` whether or not MCP is enabled, so guard it like any other read.
662
+ - **A prompt is a screen: `page().prompt(name, description)`.** There is no `prompt()` on `endpoint()`.
663
+ `prompts/get` runs the page's body in the RSC worker under the caller's token — nothing is rendered — and answers
664
+ the description, one `resource` per `fetch.*` the page made (masked by the endpoint's return model, addressed by
665
+ its `akan://` uri, one document once whatever shapes read it) and the published tools of the modules it fetched
666
+ from, minus what is already attached; lists — only lists — are cut to `promptBudget`. A guard refusing a query
667
+ inside the body reads as the screen refusing the account, the same way a redirect does. A
668
+ required argument left out answers a pointer to the `<model>List…` tool, never a guess. The description is the
669
+ only instruction, in English — `<Agent.Guide>` never reaches MCP, and the in-page chat lists no page prompts.
666
670
 
667
671
  Full contract — configuration, wire behaviour, resource URIs, OAuth metadata, protocol revisions, and
668
672
  `McpProgress.report`: `get_guideline` with `mcpRule`, or `akan guideline show mcpRule`.
@@ -794,39 +798,50 @@ shape, so `cascade` never means "related" — it means one of exactly these:
794
798
  - Reserved `_*.tsx` route filenames are limited to `_index.tsx`, `_layout.tsx`, and `_overrides.tsx`; do not add files like `_Component.tsx` or `_helper.tsx`.
795
799
  - Page filenames must not start with an uppercase letter. Move helper components like `Component.tsx` to app `ui`, `common`, or `lib` instead.
796
800
  - Dynamic segments use `[id]`; route groups use directories like `(user)`, `(public)`, `(tab)`, or `(detail)`.
797
- - Page modules should usually export `default`, `pageConfig`, `head`, `generateHead`, or `Loading`.
801
+ - **A route file exports one thing**: `export default page()…render(…)` for a page, `layout()…render(…)` for a
802
+ `_layout.tsx`, `rootLayout()…render(…)` for the app's (or a basePath's) root `_layout.tsx`. Every route setting
803
+ is a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()` / `.metadata()`, `.loading()`;
804
+ `.notFound()` / `.error()` on a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()`
805
+ / `.layoutStyle()` / `.gaTrackingId()` on the root layout; `.prompt()` on a page. A named export beside the chain,
806
+ or `page()` in a `_layout.tsx`, fails the build.
807
+ - **A page names every `[x]` segment of its path with `.param("x", Type)`** and reads a query key only through
808
+ `.search("k", Type)`. Values arrive typed — `ID`/`String` → string, `Int`/`Float` → number, `Boolean`, `Date` →
809
+ Dayjs, an `enumOf` class → its union, `[T]` → array — a path value the type refuses answers not-found, and a
810
+ search value that fails is dropped. A layout may declare a subset. Names are string literals: `akan sync` reads
811
+ them off the source and refuses a `[projectId]` folder whose page declares no `.param("projectId")`. `lang` is
812
+ never declared: every route sits under `/:lang`, and the locale segment reaches every stage as `lang`.
798
813
  - `_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.
799
- - Prefer `export default function Page` or `export default async function Page` for page components.
814
+ - The legacy shape — `export default async function Page` beside `pageConfig` / `head` / `Loading` exports — still
815
+ loads and logs one deprecation warning per file at boot. Migrate with the `workspaceRecipes` recipe "Migrating
816
+ `page/` to the route chain"; new files are never written in it.
800
817
  - `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.
801
818
  - `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.
802
819
  - 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.
803
- - `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.
820
+ - `.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.
804
821
  - Before changing route behavior, check `pkgs/akanjs/server/routeTreeBuilder.ts` and nearby routes for the expected pattern.
805
822
 
806
823
  ### Page Body Shape
807
824
 
808
825
  ```tsx
809
- interface PageProps {
810
- params: { orgId: string };
811
- }
812
-
813
- export default async function Page({ params }: PageProps) {
814
- const { l } = usePage();
815
- getSelf({ unauthorize: "/signin" });
816
- const { orgId } = params;
817
- const [{ org }, { taskInitInOrg }] = await Promise.all([fetch.viewOrg(orgId), fetch.initTaskInOrg(orgId)]);
818
- return <Task.Zone.Card init={taskInitInOrg} prefix={`/org/${orgId}`} />;
819
- }
820
-
821
- export const pageConfig = { transition: "stack" } satisfies PageConfig;
826
+ export default page()
827
+ .param("orgId", ID)
828
+ .config({ transition: "stack" })
829
+ .render(async ({ orgId }) => {
830
+ const { l } = usePage();
831
+ getSelf({ unauthorize: "/signin" });
832
+ const [{ org }, { taskInitInOrg }] = await Promise.all([fetch.viewOrg(orgId), fetch.initTaskInOrg(orgId)]);
833
+ return <Task.Zone.Card init={taskInitInOrg} prefix={`/org/${orgId}`} />;
834
+ });
822
835
  ```
823
836
 
824
- - There is no `loader=` / `render=` page prop. Pages are `export default async function Page`.
825
- - Declare `interface PageProps { params: {...}; searchParams?: {...} }` immediately above the default export.
826
- - Body order: `usePage()`, auth, destructure params, fetch, return.
837
+ - There is no `loader=` / `render=` page prop and no `PageProps` interface: the chain declares the arguments and
838
+ `.render()` receives them flat, typed, with `lang` on every route and `children` added for a layout.
839
+ - Body order inside `.render()`: `usePage()`, auth, fetch, return.
827
840
  - Run independent fetches through `Promise.all`, even when there is only one.
828
841
  - Gate auth at `_layout.tsx`; repeating `getSelf({ unauthorize: "/signin" })` in the page is fine and common.
829
- - Keep `async` even when nothing is awaited — it marks a real server page.
842
+ - `async` on the render callback only when the body awaits; the callback is not a React component, so it takes no
843
+ name and no return type.
844
+ - Publish a screen to agents with `.prompt(name, description)` — see MCP Exposure.
830
845
  - No `useState`, no `useEffect`, and no comments in page files.
831
846
 
832
847
  ## Akan Sync Conventions (`apps/**`, `libs/**`)
@@ -73,7 +73,7 @@ export const option = new AkanOption<ModulesOptions>().setMcp({
73
73
  ```
74
74
 
75
75
  ```typescript
76
- // <model>.signal.ts — every one of these is an MCP tool or prompt, with no `mcp:` option anywhere
76
+ // <model>.signal.ts — every one of these is an MCP tool, with no `mcp:` option anywhere
77
77
  export class TaskSlice extends slice(
78
78
  srv.task,
79
79
  { guards: { root: Admin, get: SignedIn, cru: SignedIn } },
@@ -86,21 +86,26 @@ export class TaskSlice extends slice(
86
86
  }),
87
87
  ) {}
88
88
 
89
- export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => ({
89
+ export class TaskEndpoint extends endpoint(srv.task, ({ mutation }) => ({
90
90
  startTask: mutation(cnst.Task, { guards: [SignedIn] })
91
91
  .param("taskId", ID)
92
92
  .exec(async function (taskId) {
93
93
  return await this.taskService.startTask(taskId);
94
94
  }),
95
- reviewTask: prompt({ guards: [SignedIn] })
96
- .param("taskId", ID)
97
- .exec(async function (taskId) {
98
- const task = await this.taskService.getTask(taskId);
99
- return [Msg.user(`Review this task and suggest next steps.`), Msg.resource(`akan://task/${taskId}`, task)];
100
- }),
101
95
  })) {}
102
96
  ```
103
97
 
98
+ ```tsx
99
+ // page/task/[taskId]/_index.tsx — the screen, published to agents as the prompt `reviewTask`
100
+ export default page()
101
+ .param("taskId", ID, { desc: "The task to review." })
102
+ .prompt("reviewTask", "Review this task and suggest next steps.")
103
+ .render(async ({ taskId }) => {
104
+ const { taskView } = await fetch.viewTask(taskId);
105
+ return <Task.Zone.View view={taskView} />;
106
+ });
107
+ ```
108
+
104
109
  ## Refusals And Wire Behaviour
105
110
  - **`mcp: false` is read first**, because it is the one answer nobody had to derive — somebody stated it. Its
106
111
  reason says "HTTP still serves it", because that is the sentence that stops the next reader treating it as a lock.
@@ -111,16 +116,13 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => (
111
116
  **`light<Model>` read** — it returns the same document as `<Model>` under the same guards, in a shape trimmed
112
117
  for a page's payload, so publishing both put two entries per model in a listing an agent pays for every turn
113
118
  and gave it nothing but the word "light" to choose between them.
114
- A `prompt` refuses two more, because its `arguments` is one string per name with no schema beside it: a **list
115
- argument**, which could never carry a second value, and **any `Any` argument** — a tool leaves that out of its
116
- schema, and a prompt has no schema to leave it out of.
117
119
  - **The boot log sizes the catalogue and says which signals it went to** — `MCP catalogue: tools=48 … · listing
118
120
  214KB` and `MCP catalogue cost: user 27/71KB · file 6/22KB · …`, heaviest first. Read it before narrowing
119
121
  anything: the number is nobody's intuition, because MCP has no shared component section and forbids a `$ref`
120
122
  across entries, so **every entry inlines the full schema of every model it mentions** and a listing is re-sent
121
123
  whole to every agent that connects, before its first turn. Past 100KB it says so as a `warn`.
122
- - **Every refusal is named in the boot log**: one `warn` per endpoint plus a `MCP catalogue: tools=… prompts=…`
123
- count. Read that line first when a tool you expected is missing — and it is the *only* place the answer exists,
124
+ - **Every refusal is named in the boot log**: one `warn` per endpoint plus a `MCP catalogue: tools=…
125
+ resourceTemplates=…` count, and a `MCP page prompts: N (…)` line once the RSC worker has listed the pages. Read that line first when a tool you expected is missing — and it is the *only* place the answer exists,
124
126
  because there is no absent opt-in to notice. The API explorer badges the same rule per endpoint (`MCP` /
125
127
  `MCP refused`), from the same shared implementation the catalogue runs.
126
128
  - **An `Any` argument is left out of the published schema** rather than described as `{}` — it tells a model
@@ -146,7 +148,8 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => (
146
148
  doubling of what every model-returning tool costs a model. `option.setMcp({ legacyTextBlock: false })` — or
147
149
  `AKAN_MCP_LEGACY_TEXT=false` — leaves a one-line pointer in the text block instead, for a deployment whose
148
150
  clients read the structured half. A scalar return is unaffected: it has no structured half to point at, so it
149
- ships as the value itself either way.
151
+ ships as the value itself either way. `resources/read` is unaffected too: `ReadResourceResult` has no
152
+ `structuredContent`, so a resource's text is its only channel and always carries the serialized value.
150
153
  - **A request schema asks for a relation's id, and a response schema names a nested model.** `serialize` sends a
151
154
  relation field as its id and the server never converts an object back, so publishing the related model's shape
152
155
  asked an agent for what the document layer cannot take. On the response side every entry inlines the transitive
@@ -213,9 +216,9 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => (
213
216
  which is in force, and warns when it is off.
214
217
  - **A `resources/read` uri that does not decode** — a stray `%` — is `Unknown resource`, not a server failure.
215
218
  - **A caller's own mistake is reported as one** and never as a server failure: an argument that is missing,
216
- unparseable or **undeclared** comes back as `isError` naming it — `additionalProperties: false` travels in the
219
+ unparseable, outside its `enumOf`, or **undeclared** comes back as `isError` naming it — `additionalProperties: false` travels in the
217
220
  published schema and nothing on the wire enforces it — and so does a document that is not there, as
218
- `No <model> found for the arguments given.` A `prompt`, having no `isError` to carry a refusal, answers `-32602`.
221
+ `No <model> found for the arguments given.` A prompt, having no `isError` to carry a refusal, answers `-32602`.
219
222
  Only a real failure logs a stack; an agent can drive the rest at will.
220
223
  - **Three revisions are spoken**: the modern `2026-07-28` and the legacy `2025-11-25` / `2025-06-18`, which are
221
224
  wire-identical over the POST-only surface this implements — a client whose proposal is not listed is told to
@@ -225,7 +228,7 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => (
225
228
  - **A modern-era request mirrors `MCP-Protocol-Version` and `Mcp-Method` into headers** (plus `Mcp-Name` when the
226
229
  body names one), and one that leaves a mirror out is refused just like one that contradicts the body: a gateway
227
230
  rule keyed on a header never fires for the request that omitted it. Legacy requests are not checked. Capabilities
228
- are derived from the catalogue, so a server with no prompts does not advertise `prompts`.
231
+ are derived from the catalogue; `prompts` is advertised when the build serves pages, since prompts are theirs.
229
232
  - **An expired, wrongly-audienced or unverifiable bearer token is refused up front**, so an agent is told to
230
233
  authenticate rather than that the tool does not exist. The signature is checked through `auth.verify`, which the
231
234
  module that mints the tokens supplies (`libs/shared` does, from the app's signing secret); without a verifier the
@@ -313,23 +316,41 @@ delegates to lives in `akanjs/server` (`OAuthAuthorize`, `OAuthToken`, `OAuthReg
313
316
  derived fallback is seeded from the app, environment and repo names and forges an admin token for anyone who
314
317
  knows them. `AKAN_ALLOW_DERIVED_JWT_SECRET=1` accepts that risk explicitly.
315
318
 
316
- ## `prompt()`
317
- **`prompt()`** is invoked by the *user* — a client renders it as a slash command — not chosen by the model. `exec`
318
- returns `PromptMessage[]`, or a bare string that is wrapped into one user message; build them with `Msg.user` /
319
- `Msg.assistant` / `Msg.link` / `Msg.resource` / `Msg.image` / `Msg.imageOf`. It takes `.param()` and `.search()`
320
- only, because `prompts/get` sends a flat string map. **An embedded payload is masked by the model you name** —
321
- `Msg.resource(uri, task, { model: cnst.LightTask })`, or `Msg.mask(cnst.LightTask, task)` for one piece of an
322
- assembly. Taking the model as an argument is what makes a `{ ...doc }` spread maskable, since that and `toJSON()`
323
- arrive with the class already gone; a value with no model named whose `hidden`/`secret` fields are populated is
324
- **refused**, one level into a plain object too. **A `prompt` is also mounted as a
325
- plain HTTP `GET` whether or not you enabled MCP**, because that route is what lets a web UI preview one — and it
326
- is in your OpenAPI document like any other `GET`, answering the one fixed `PromptMessage[]` shape. MCP exposure
327
- gates the catalogue, not the surface, so guard it
328
- like any other read — and a prompt declaring no
329
- `guards` at all is named in the boot log, while an explicit `[Public]` is a decision and stays quiet. Every `Msg` builder takes
330
- optional `annotations` last (`audience`, `priority` 0..1, `lastModified`) — give the instruction a high `priority`
331
- and its attachments a low one, or a client with a full window drops blocks by position and keeps the attachment
332
- over the ask.
319
+ ## Page Prompts — `page().prompt()`
320
+ **A prompt is a screen.** There is no `prompt()` on `endpoint()`: the page file declares it —
321
+ `page().param("projectId", ID, { desc: "The project to brief." }).prompt("briefProjectTickets", "Brief the ticket
322
+ board of one project.").render(…)` — and that is the whole surface. `prompts/list` is every page carrying
323
+ `.prompt()`; its `.param()` stages are the required arguments and its `.search()` stages the optional ones, a list
324
+ argument is comma-separated on the wire and its description says so, and an `ID`/`Int`/`enumOf` value is checked by
325
+ the page's own declaration. **`prompts/get` runs the page's body** — root layouts, layouts, then the page's
326
+ `.render()` — in the RSC worker under the caller's bearer token, the way a tab would open it. Nothing is rendered:
327
+ the JSX comes back as an element tree nobody walks, no client component runs, and the screen's data footprint is
328
+ exactly the `fetch.*` queries its body made, with the arguments it made them with. The answer is the description as
329
+ the first message (priority 1), one embedded `resource` per query masked by that endpoint's return model and
330
+ addressed by the `akan://` uri the tool answers to (`akan://<toolKey>?args` for a custom read) — one document read
331
+ in two shapes, a layout's `project` beside the page's `lightProject`, travels once as the larger — and a closing
332
+ `Tools for this screen: …` line naming the published tools of the modules the screen fetched from, minus the reads
333
+ whose answers are attached above and filtered to what this caller may see — which is how a model reading a ticket
334
+ board is handed the ticket tools and not three hundred. Lists are cut to `promptBudget` (60,000 characters;
335
+ `option.setMcp({ promptBudget })`, `AKAN_MCP_PROMPT_BUDGET`), largest first, and the cut is said: `Attached the
336
+ first 20 of 341 rows of \`ticketListInProject\`; call it for the rest.` Only lists are cut — a single document rides
337
+ whole, so the budget is what the lists are trimmed to fit under, not a ceiling a screen of large documents cannot
338
+ pass. A layout's own queries (a project selector's `projectListInOrg`) are part of every screen under it and are
339
+ attached too. **No argument fallback**: a required argument left out answers one message — `No projectId was named for
340
+ "briefProjectTickets". Find it with \`projectListBySearch\`, then run this prompt again with projectId=<id>.` —
341
+ and runs nothing, because a prompt cannot re-run itself and context filled in on a guess would never be used. A
342
+ page that redirects (`getSelf({ unauthorize })`), or whose `fetch.*` a guard refuses with 401/403, answers the 401
343
+ credential challenge when the call carried no token and `This screen is not available to the signed-in account.`
344
+ when it did — the same sentence for both, so a refused id is never confirmed to exist; `router.notFound()` and a
345
+ 404 from a query are `No screen exists for these arguments.`; anything else the body throws is `The page failed to
346
+ load.`, with the cause in the server log only. **The description is the only instruction.** `<Agent.Guide>` is for the in-page agent, whose
347
+ levers are the screen's controls, and is never read here, where the levers are the API — so write the description
348
+ in English, in the API's vocabulary, and put a computed rule (the legal status transitions) into it as a static
349
+ table the model combines with the attached state. Names match `^[A-Za-z0-9_-]{1,64}$` and are unique across pages;
350
+ two pages claiming one name fail the listing. An API-only build (`web: false`) publishes no prompts, and the
351
+ in-page chat lists none of them: its `/` menu is its six built-in commands. Pick which screens to publish by this
352
+ rule: a page taking no arguments is the most useful prompt, since MCP has no completion — one taking an id is for
353
+ when the id is already in hand.
333
354
 
334
355
  ## Progress Reporting
335
356
  **`McpProgress.report(n, { total, message })`** reports progress from anywhere inside a call, a service or adapter
@@ -399,8 +420,8 @@ the same guards.** What stays closed is closed because no agent has a reason to
399
420
  ## Writing `instructions`
400
421
 
401
422
  `*.abstract.md` and `<Agent.Guide>` never reach an MCP client. Exactly three texts do: `instructions`, every
402
- published entry's `.desc()`, and a `prompt()`. All three are English, whatever the users speak — the reader is a
403
- model, and the catalogue is built once in one `language` and cached by clients.
423
+ published entry's `.desc()`, and a page's `.prompt()` description. All three are English, whatever the users speak
424
+ — the reader is a model, and the catalogue is built once in one `language` and cached by clients.
404
425
 
405
426
  - **`instructions` is the workflow.** Five to eight sentences, in this order: what the app is and what its nouns
406
427
  mean; **which tool to call first** and which ids the rest take; how a name becomes an id; what a write confirms
@@ -419,9 +440,10 @@ model, and the catalogue is built once in one `language` and cached by clients.
419
440
  - **`.desc()` is the reason to pick one tool** — what it returns and when to use it, two clauses. An id argument's
420
441
  `.desc()` names the tool that returns it (`From projectInOrg`). The generated CRUD borrows the model's `.of()`
421
442
  label and `.desc()`, which is the only text those entries can carry.
422
- - **`prompt()` is a workflow the user invokes** as a slash command, not one the model chooses — a weekly report,
423
- an onboarding check. Put the ask in `Msg.user` at a high `priority` and the attachments in `Msg.resource` masked
424
- by their `Light` model at a low one.
443
+ - **A page prompt is a workflow the user invokes** as a slash command, not one the model chooses — a weekly
444
+ report, an onboarding check. Its instruction is the `.prompt()` description alone and its attachments are what
445
+ the screen fetched, masked by their return models — so to change what a prompt hands the model, change what the
446
+ screen loads, and keep the description to the ask.
425
447
 
426
448
  ## Verifying An App
427
449