@akanjs/devkit 3.1.1-rc.9 → 3.1.2-rc.0

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 (188) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/agentAssets.test.ts +65 -0
  3. package/agentAssets.ts +166 -0
  4. package/agentsIndex.ts +4 -4
  5. package/akanApp/akanApp.host.test.ts +12 -0
  6. package/akanApp/akanApp.host.ts +12 -0
  7. package/akanConfig/akanConfig.test.ts +105 -0
  8. package/akanConfig/akanConfig.ts +21 -1
  9. package/akanConfig/productionLockfile.test.ts +31 -0
  10. package/akanConfig/productionLockfile.ts +2 -0
  11. package/akanContext.ts +129 -4
  12. package/akanMcpContract.test.ts +28 -1
  13. package/akanMcpContract.ts +51 -3
  14. package/applicationBuildRunner.test.ts +53 -0
  15. package/applicationBuildRunner.ts +7 -4
  16. package/artifact/implicitRootLayout.ts +4 -2
  17. package/biome.base.json +4 -1
  18. package/cloud/globalConfig.test.ts +34 -0
  19. package/cloud/globalConfig.ts +2 -2
  20. package/codeAgent/agent/AkanCodeEnv.ts +26 -0
  21. package/codeAgent/agent/AkanCodeServices.ts +5 -2
  22. package/codeAgent/agent/CodeAgent.ts +11 -0
  23. package/codeAgent/agent/CodeAgentGate.ts +156 -10
  24. package/codeAgent/agent/CodeAgentProxy.ts +5 -4
  25. package/codeAgent/agent/CodeAgentRpcHost.ts +3 -0
  26. package/codeAgent/agent/CodeAgentRpcListener.ts +102 -16
  27. package/codeAgent/agent/SubagentPool.ts +5 -0
  28. package/codeAgent/agent/WorkspaceTrust.ts +200 -0
  29. package/codeAgent/agent/akanCodePaths.ts +8 -1
  30. package/codeAgent/agent/akanSystemPrompt.ts +5 -1
  31. package/codeAgent/agent/codeAgent.test.ts +41 -2
  32. package/codeAgent/agent/codeAgentContract.test.ts +119 -0
  33. package/codeAgent/agent/codeAgentHosts.ts +30 -0
  34. package/codeAgent/agent/codeAgentProxy.test.ts +53 -1
  35. package/codeAgent/agent/codeAgentRpcListener.test.ts +115 -13
  36. package/codeAgent/agent/stringArg.ts +1 -1
  37. package/codeAgent/feedback/PreviewFeedback.ts +2 -2
  38. package/codeAgent/feedback/VerifyFeedback.ts +3 -4
  39. package/codeAgent/index.ts +2 -2
  40. package/codeAgent/skills/akan-agent/SKILL.md +43 -0
  41. package/codeAgent/skills/akan-code/SKILL.md +38 -11
  42. package/codeAgent/skills/akan-debug/SKILL.md +41 -6
  43. package/codeAgent/skills/akan-deps/SKILL.md +1 -2
  44. package/codeAgent/skills/akan-err/SKILL.md +2 -1
  45. package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
  46. package/codeAgent/skills/akan-page/SKILL.md +44 -0
  47. package/codeAgent/skills/akan-query/SKILL.md +39 -0
  48. package/codeAgent/skills/akan-runtime/SKILL.md +43 -0
  49. package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
  50. package/codeAgent/skills/akan-transport/SKILL.md +40 -0
  51. package/codeAgent/skills/akan-validate/SKILL.md +13 -11
  52. package/codeAgent/tools/AkanCli.ts +2 -1
  53. package/codeAgent/tools/AkanCodePlugins.ts +5 -1
  54. package/codeAgent/tools/AkanEditScope.ts +8 -3
  55. package/codeAgent/tools/AkanToolPack.ts +4 -10
  56. package/codeAgent/tools/AkanVerifier.ts +13 -31
  57. package/codeAgent/tools/McpServerConfig.ts +6 -4
  58. package/codeAgent/tools/McpSignIn.ts +7 -7
  59. package/codeAgent/tools/McpTokenStore.ts +63 -16
  60. package/codeAgent/tools/McpToolPack.ts +8 -1
  61. package/codeAgent/tools/WebToolPack.ts +50 -6
  62. package/codeAgent/tools/akanCodePlugins.test.ts +130 -60
  63. package/codeAgent/tools/mcpOAuth.test.ts +50 -14
  64. package/codeAgent/tools/mcpServerConfig.test.ts +130 -1
  65. package/commandDecorators/command.ts +4 -1
  66. package/commandDecorators/workspaceVersionCheck.test.ts +17 -7
  67. package/commandDecorators/workspaceVersionCheck.ts +4 -1
  68. package/commentDensityScanner.test.ts +121 -0
  69. package/commentDensityScanner.ts +101 -0
  70. package/componentSizeScanner.test.ts +36 -0
  71. package/componentSizeScanner.ts +52 -0
  72. package/componentWriteChainScanner.test.ts +137 -0
  73. package/componentWriteChainScanner.ts +104 -0
  74. package/conventionSet.test.ts +73 -0
  75. package/conventionSet.ts +67 -0
  76. package/createTunnel.test.ts +200 -2
  77. package/createTunnel.ts +80 -6
  78. package/csrE2e/ssrRegistry.e2e.test.ts +6 -1
  79. package/deprecatedCommentScanner.test.ts +54 -0
  80. package/deprecatedCommentScanner.ts +30 -0
  81. package/endpointReach.test.ts +58 -0
  82. package/endpointReach.ts +80 -0
  83. package/envSeed.test.ts +90 -0
  84. package/envSeed.ts +68 -0
  85. package/executors.test.ts +19 -5
  86. package/executors.ts +59 -4
  87. package/fileSys.test.ts +30 -2
  88. package/fileSys.ts +14 -0
  89. package/formSetterScanner.test.ts +44 -1
  90. package/formSetterScanner.ts +53 -8
  91. package/frontendBuild/csrArtifactBuilder.ts +7 -0
  92. package/frontendBuild/mcpAppViewBuilder.test.ts +63 -0
  93. package/frontendBuild/mcpAppViewBuilder.ts +7 -1
  94. package/frontendBuild/optionalPeers.test.ts +132 -0
  95. package/index.ts +1 -0
  96. package/integration/devStability.integration.test.ts +9 -5
  97. package/integration/ssrMemoryProbe.ts +6 -0
  98. package/kindBranchScanner.test.ts +89 -0
  99. package/kindBranchScanner.ts +158 -0
  100. package/lint/__fixtures__/no-oversized-list-limit/bad.tsx +18 -0
  101. package/lint/__fixtures__/no-oversized-list-limit/good.tsx +23 -0
  102. package/lint/no-oversized-list-limit.grit +73 -0
  103. package/lint/no-unguarded-endpoint.grit +5 -5
  104. package/linter.test.ts +12 -0
  105. package/linter.ts +35 -12
  106. package/maintainabilityBaseline.test.ts +280 -0
  107. package/maintainabilityBaseline.ts +220 -0
  108. package/maintainabilityContract.test.ts +846 -0
  109. package/maintainabilityContract.ts +404 -0
  110. package/maintainabilityExceptionScanner.test.ts +90 -0
  111. package/maintainabilityExceptionScanner.ts +85 -0
  112. package/maintainabilityLimits.ts +60 -0
  113. package/maintainabilityReport.test.ts +130 -0
  114. package/maintainabilityReport.ts +153 -0
  115. package/mobile/desktopRelease.test.ts +1 -1
  116. package/mobile/desktopRelease.ts +3 -1
  117. package/mobile/desktopServerStage.test.ts +1 -0
  118. package/mobile/desktopServerStage.ts +3 -1
  119. package/mobile/nativeApp.test.ts +20 -5
  120. package/mobile/nativeApp.ts +4 -1
  121. package/mobile/nativeConfig.test.ts +209 -5
  122. package/mobile/nativeConfig.ts +127 -6
  123. package/mobile/nativePublicEnv.test.ts +2 -0
  124. package/mobile/nativePublicEnv.ts +13 -0
  125. package/package.json +4 -2
  126. package/platformTest/LinuxDockerTarget.ts +3 -0
  127. package/{codeAgent/feedback → preview}/PreviewChrome.ts +13 -7
  128. package/preview/PreviewView.ts +219 -0
  129. package/preview/index.ts +2 -0
  130. package/preview/previewView.test.ts +76 -0
  131. package/prompter.ts +7 -0
  132. package/qualityScanner.test.ts +75 -0
  133. package/qualityScanner.ts +17 -1
  134. package/routeSourceValidator.test.ts +11 -2
  135. package/routeSourceValidator.ts +14 -2
  136. package/serviceLoopCallScanner.test.ts +56 -0
  137. package/serviceLoopCallScanner.ts +122 -0
  138. package/shellOnlyEnv.test.ts +65 -0
  139. package/shellOnlyEnv.ts +54 -0
  140. package/signalMutationIndex.test.ts +15 -0
  141. package/signalMutationIndex.ts +23 -82
  142. package/sourceComments.test.ts +45 -0
  143. package/sourceComments.ts +70 -0
  144. package/sshHostKeys.ts +59 -0
  145. package/storeAccessScanner.test.ts +84 -0
  146. package/storeAccessScanner.ts +71 -0
  147. package/storeSource.ts +160 -0
  148. package/storeWriteScanner.test.ts +292 -0
  149. package/storeWriteScanner.ts +50 -0
  150. package/subspace.test.ts +10 -0
  151. package/subspaceClone.ts +3 -1
  152. package/subspaceConfig.ts +6 -0
  153. package/testOnlyExportScanner.test.ts +83 -0
  154. package/testOnlyExportScanner.ts +124 -0
  155. package/transforms/barrelImportsPlugin.ts +10 -4
  156. package/transforms/mcpAppPageTrimmer.test.ts +8 -2
  157. package/transforms/mcpAppPageTrimmer.ts +5 -20
  158. package/transforms/pureLazyMarker.ts +1 -1
  159. package/typeChecker.ts +32 -0
  160. package/typecheck/projectTypecheck.test.ts +18 -0
  161. package/typecheck/projectTypecheck.ts +29 -5
  162. package/typecheck/typecheck.proc.ts +6 -0
  163. package/typecheck/typecheckFailure.ts +13 -0
  164. package/types.ts +1 -0
  165. package/workflow/artifacts.ts +16 -2
  166. package/workflow/executor.ts +22 -7
  167. package/workflow/guardCatalog.test.ts +59 -0
  168. package/workflow/guardCatalog.ts +108 -0
  169. package/workflow/index.ts +4 -0
  170. package/workflow/interactionScaffold.ts +122 -0
  171. package/workflow/mutationScaffold.ts +533 -0
  172. package/workflow/mutationSource.test.ts +108 -0
  173. package/workflow/mutationSource.ts +219 -0
  174. package/workflow/plan.ts +82 -0
  175. package/workflow/render.ts +19 -4
  176. package/workflow/source.test.ts +185 -0
  177. package/workflow/source.ts +151 -24
  178. package/workflow/sourceAst.ts +10 -48
  179. package/workflow/types.ts +18 -1
  180. package/workspaceLayout.ts +2 -0
  181. package/writePath.test.ts +109 -0
  182. package/writePath.ts +219 -0
  183. package/writePlacementScanner.test.ts +77 -0
  184. package/writePlacementScanner.ts +50 -0
  185. package/writingActionIndex.test.ts +99 -0
  186. package/writingActionIndex.ts +63 -0
  187. package/codeAgent/feedback/PreviewView.ts +0 -95
  188. /package/{codeAgent/feedback → preview}/previewChrome.test.ts +0 -0
@@ -1,5 +1,5 @@
1
1
  import { afterAll, describe, expect, test } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
2
+ import { mkdtempSync, rmSync, statSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import path from "node:path";
5
5
  import type { CodeAgentEvent } from "akanjs/common";
@@ -44,23 +44,28 @@ class FakeAgent {
44
44
 
45
45
  class Client {
46
46
  lines: unknown[] = [];
47
+ readonly closed = Promise.withResolvers<void>();
47
48
  #buffer = "";
48
49
  #socket: Awaited<ReturnType<typeof Bun.connect>> | null = null;
49
- static async connect(unix: string) {
50
+ static async connect(target: string | { port: number }) {
50
51
  const client = new Client();
51
- client.#socket = await Bun.connect({
52
- unix,
53
- socket: {
54
- data: (_socket, data) => {
55
- client.#buffer += data.toString();
56
- const parts = client.#buffer.split("\n");
57
- client.#buffer = parts.pop() ?? "";
58
- for (const part of parts) if (part) client.lines.push(JSON.parse(part));
59
- },
52
+ const socket = {
53
+ data: (_socket: unknown, data: Buffer) => {
54
+ client.#buffer += data.toString();
55
+ const parts = client.#buffer.split("\n");
56
+ client.#buffer = parts.pop() ?? "";
57
+ for (const part of parts) if (part) client.lines.push(JSON.parse(part));
60
58
  },
61
- });
59
+ close: () => client.closed.resolve(),
60
+ };
61
+ client.#socket = await (typeof target === "string"
62
+ ? Bun.connect({ unix: target, socket })
63
+ : Bun.connect({ hostname: "127.0.0.1", port: target.port, socket }));
62
64
  return client;
63
65
  }
66
+ raw(text: string) {
67
+ this.#socket?.write(text);
68
+ }
64
69
  send(id: string, command: object) {
65
70
  this.#socket?.write(`${JSON.stringify({ id, command })}\n`);
66
71
  }
@@ -81,10 +86,17 @@ describe("CodeAgentRpcListener", () => {
81
86
  test("parses unix and tcp addresses, defaulting tcp to loopback", () => {
82
87
  expect(CodeAgentRpcListener.parse("unix:/tmp/a.sock")).toEqual({ unix: "/tmp/a.sock" });
83
88
  expect(CodeAgentRpcListener.parse("tcp:7000")).toEqual({ hostname: "127.0.0.1", port: 7000 });
84
- expect(CodeAgentRpcListener.parse("0.0.0.0:7000")).toEqual({ hostname: "0.0.0.0", port: 7000 });
89
+ expect(CodeAgentRpcListener.parse("tcp::7000")).toEqual({ hostname: "127.0.0.1", port: 7000 });
90
+ expect(CodeAgentRpcListener.parse("localhost:7000")).toEqual({ hostname: "localhost", port: 7000 });
85
91
  expect(() => CodeAgentRpcListener.parse("tcp:nope")).toThrow("--rpc-listen");
86
92
  });
87
93
 
94
+ test("a bind other machines could reach is refused unless it is asked for", () => {
95
+ expect(() => CodeAgentRpcListener.parse("0.0.0.0:7000")).toThrow("--rpc-bind-any");
96
+ expect(() => CodeAgentRpcListener.parse("tcp:192.168.0.2:7000")).toThrow("--rpc-bind-any");
97
+ expect(CodeAgentRpcListener.parse("0.0.0.0:7000", { anyHost: true })).toEqual({ hostname: "0.0.0.0", port: 7000 });
98
+ });
99
+
88
100
  test("the agent outlives a dropped client, and the next one catches up by seq", async () => {
89
101
  const agent = new FakeAgent();
90
102
  const unix = path.join(dir, "agent.sock");
@@ -110,4 +122,94 @@ describe("CodeAgentRpcListener", () => {
110
122
  await served;
111
123
  expect(agent.disposed).toBe(true);
112
124
  });
125
+
126
+ describe("over tcp", () => {
127
+ const opened = () => {
128
+ const agent = new FakeAgent();
129
+ const listener = new CodeAgentRpcListener(new CodeAgentRpcHost(agent as unknown as CodeAgent, null), {
130
+ hostname: "127.0.0.1",
131
+ port: 0,
132
+ }).listen();
133
+ return { agent, listener, port: listener.port ?? 0 };
134
+ };
135
+
136
+ test("a connection opened with the token is answered, and one without it is closed unheard", async () => {
137
+ const { agent, listener, port } = opened();
138
+ try {
139
+ expect(listener.token).toMatch(/^[A-Za-z0-9_-]{43}$/);
140
+ const anonymous = await Client.connect({ port });
141
+ anonymous.send("1", { type: "prompt", message: "hi" });
142
+ await anonymous.closed.promise;
143
+ expect(anonymous.lines).toEqual([{ type: "reply", id: "auth", ok: false, error: "unauthorized" }]);
144
+
145
+ const wrong = await Client.connect({ port });
146
+ wrong.raw(`${JSON.stringify({ auth: "not-the-token" })}\n`);
147
+ await wrong.closed.promise;
148
+ expect(agent.seq).toBe(1);
149
+
150
+ const client = await Client.connect({ port });
151
+ client.raw(
152
+ `${JSON.stringify({ auth: listener.token })}\n${JSON.stringify({ id: "2", command: { type: "prompt", message: "hi" } })}\n`,
153
+ );
154
+ expect((await client.reply("auth")).ok).toBe(true);
155
+ expect((await client.reply("2")).ok).toBe(true);
156
+ expect(client.lines.findIndex((line) => (line as { id?: string }).id === "auth")).toBe(0);
157
+ client.close();
158
+ } finally {
159
+ listener.stop();
160
+ }
161
+ });
162
+
163
+ test("a connection that speaks anything but a JSON frame is dropped at its first byte", async () => {
164
+ const { listener, port } = opened();
165
+ try {
166
+ const browser = await Client.connect({ port });
167
+ browser.raw("GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n");
168
+ await browser.closed.promise;
169
+ expect(browser.lines).toEqual([]);
170
+ } finally {
171
+ listener.stop();
172
+ }
173
+ });
174
+
175
+ test("a connection that has not authenticated does not displace the client that has", async () => {
176
+ const { listener, port } = opened();
177
+ try {
178
+ const client = await Client.connect({ port });
179
+ client.raw(`${JSON.stringify({ auth: listener.token })}\n`);
180
+ await client.reply("auth");
181
+ const other = await Client.connect({ port });
182
+ other.raw("{");
183
+ await Bun.sleep(30);
184
+ client.send("3", { type: "get_state", sinceSeq: 0 });
185
+ expect((await client.reply("3")).ok).toBe(true);
186
+ other.close();
187
+ client.close();
188
+ } finally {
189
+ listener.stop();
190
+ }
191
+ });
192
+ });
193
+
194
+ test.skipIf(process.platform === "win32")(
195
+ "a unix socket is its owner's alone, and needs no token, though one is accepted",
196
+ async () => {
197
+ const agent = new FakeAgent();
198
+ const unix = path.join(dir, "owned.sock");
199
+ const listener = new CodeAgentRpcListener(new CodeAgentRpcHost(agent as unknown as CodeAgent, null), { unix });
200
+ listener.listen();
201
+ try {
202
+ expect(listener.token).toBeNull();
203
+ expect(statSync(unix).mode & 0o777).toBe(0o600);
204
+ const client = await Client.connect(unix);
205
+ client.raw(`${JSON.stringify({ auth: "anything" })}\n`);
206
+ expect((await client.reply("auth")).ok).toBe(true);
207
+ client.send("4", { type: "get_state", sinceSeq: 0 });
208
+ expect((await client.reply("4")).ok).toBe(true);
209
+ client.close();
210
+ } finally {
211
+ listener.stop();
212
+ }
213
+ },
214
+ );
113
215
  });
@@ -1,4 +1,4 @@
1
- export const stringArg = (args: unknown, key: "path" | "command") => {
1
+ export const stringArg = (args: unknown, key: "path" | "command" | "url") => {
2
2
  if (!args || typeof args !== "object") return undefined;
3
3
  const value = (args as Record<string, unknown>)[key];
4
4
  return typeof value === "string" && value ? value : undefined;
@@ -1,5 +1,5 @@
1
+ import { PreviewView } from "../../preview/PreviewView";
1
2
  import { AkanEditScope } from "../tools/AkanEditScope";
2
- import { PreviewView } from "./PreviewView";
3
3
  import type { TurnFeedbackFinding, TurnFeedbackSource } from "./TurnFeedback";
4
4
 
5
5
  export interface PreviewFeedbackOptions {
@@ -35,7 +35,7 @@ export class PreviewFeedback implements TurnFeedbackSource {
35
35
  for (const route of this.#options.routes ?? ["/en/"]) {
36
36
  const url = new URL(route, this.#options.previewUrl).toString();
37
37
  const probe = await PreviewView.probe(url, { screenshot: !!this.#options.canSeeImages });
38
- const problems = [...probe.pageErrors, ...probe.consoleErrors, ...probe.domFindings];
38
+ const problems = [...probe.pageErrors, ...probe.consoleErrors, ...probe.consoleWarnings, ...probe.domFindings];
39
39
  if (!problems.length) continue;
40
40
  findings.push(`**${url}**\n${problems.map((line) => `- ${line}`).join("\n")}`);
41
41
  if (probe.screenshot) images.push({ data: probe.screenshot, mimeType: "image/png" });
@@ -26,11 +26,10 @@ export class VerifyFeedback implements TurnFeedbackSource {
26
26
 
27
27
  async observe() {
28
28
  const scope = await AkanEditScope.since(this.#options.cwd, this.#baseline);
29
- if (!scope.apps.length && !scope.libs.length) return undefined;
29
+ if (!scope.apps.length && !scope.libs.length && !scope.pkgs.length) return undefined;
30
30
  if (this.#options.readOnly) return VerifyFeedback.#readOnlyNotice(scope.needsSync, [...scope.apps, ...scope.libs]);
31
- const report = await this.#verifier.verify(scope);
32
- if (report.ok) return undefined;
33
- return AkanVerifier.summarize(report);
31
+ const report = await this.#verifier.verify(scope.paths);
32
+ return report.ok ? undefined : report.text;
34
33
  }
35
34
 
36
35
  static #readOnlyNotice(needsSync: boolean, targets: string[]) {
@@ -1,5 +1,7 @@
1
1
  // Importing this barrel loads the engine (~122MiB resident); a host needing cheap pieces imports the owning module.
2
2
  // agent/consoleToStderr is deliberately absent: it takes over globalThis.console, so an RPC entry imports it first.
3
+
4
+ export * from "../preview";
3
5
  export * from "./agent/AkanCodeCompaction";
4
6
  export * from "./agent/AkanCodeServices";
5
7
  export * from "./agent/akanCodeModel";
@@ -17,9 +19,7 @@ export * from "./agent/CodeSessionFork";
17
19
  export * from "./agent/CodeSessionIndex";
18
20
  export * from "./agent/SubagentPool";
19
21
  export * from "./feedback/DevLogFeedback";
20
- export * from "./feedback/PreviewChrome";
21
22
  export * from "./feedback/PreviewFeedback";
22
- export * from "./feedback/PreviewView";
23
23
  export * from "./feedback/TurnFeedback";
24
24
  export * from "./feedback/VerifyFeedback";
25
25
  export * from "./tools/AkanCli";
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: akan-agent
3
+ description: "Build the in-page agent: mounting Agent.Chat, setAgentAccess and the request policy, the LLM, publishing st.tool actions and cards, forms, readable state, zones, route guidance and slash commands."
4
+ ---
5
+
6
+ # Akan in-page agent
7
+
8
+ **The surface is what components declare, exactly.** `st.tool` publishes one action and `st.use` / `st.sel` /
9
+ `st.ref` make one store key readable while the reading component is mounted. A method on a store class gives an
10
+ agent nothing, and only the current page's declarations are live.
11
+
12
+ ## Mount it
13
+
14
+ 1. Mount `<Agent.Chat />` once, in a layout. It never executes tools: every tool runs in the caller's browser,
15
+ gated by guards and the approval card.
16
+ 2. It refuses every call until `option.setAgentAccess(<guard>)` in `lib/option.ts` names who may use it
17
+ (`SignedIn` for a product with accounts). What a turn may ask is `option.setAgentRequestPolicy`; until one is
18
+ set, a turn past `AgentRelayPolicy.defaultLimits` is refused with 413, and a policy of the app's own keeps them
19
+ with `AgentRelayPolicy.capped(request, limits)`. An error that is not an `Err` reaches the chat as a generic
20
+ failure unless `AKAN_ERROR_DETAIL=1` or a development runtime shows it.
21
+ 3. The LLM is `option.setLlm({ apiKey, model, host })`, never env.
22
+
23
+ ## Publish actions
24
+
25
+ - Declare the tool beside the control that already does it, and hand the result to the control:
26
+ `st.tool("x").desc("…").arg("id", ID).exec((id) => st.do.removeX(id))`. One handler serves the person and the
27
+ agent; `.desc()` is required and first.
28
+ - A name starting with a write verb (`remove`, `delete`, `create`, `update`, `upsert`, `submit`, `save`, `send`,
29
+ `export`) asks the user first; `{ confirm: false }` opts one tool out, `AgentConfirm.set` the app.
30
+ - `.card(({ submit, cancel }) => <Form … />)` parks the call in the chat for an answer that is the user's to give.
31
+ - Store-driven forms publish themselves: `onChange={st.do.setFieldOnX}` by reference emits the `data-akan-action`
32
+ the agent drives. A pass-through arrow publishes nothing and fails lint.
33
+ - Every `st.tool(…).exec()` / `.card()` is a hook: never behind a condition.
34
+
35
+ ## Reads and guidance
36
+
37
+ - `st.use.x({ agent: false })` hides a key; `hidden`, `secret` and `visual` fields are stripped from every read.
38
+ - Page text is data the model reads, never instructions: after a screen read the next call that changes something
39
+ asks once, and `data-agent-private="<label>"` keeps a region's text out of every read.
40
+ - A tool value is capped at 20,000 characters.
41
+ - Route guidance is `<Agent.Guide instructions="…" />`; model-facing text is English.
42
+
43
+ Full detail: `get_guideline` with `agentRule`.
@@ -9,9 +9,9 @@ This is the tool the user is talking to right now. When they ask how to do somet
9
9
  with their code — answer from here, and quote the exact gesture: the slash command to type, or the file to
10
10
  edit. Do not guess a command; everything this tool offers is listed below.
11
11
 
12
- You cannot type a slash command yourself. They are the host's, not yours — say which one to type. You *can*
13
- edit the files named below, which is the useful half of most of these answers: write the change, then say
14
- which command makes it take effect.
12
+ You cannot type a slash command yourself. They are the host's, not yours — say which one to type. You cannot
13
+ edit `mcp.json`, `.akan/code/extensions/` or anything under `~/.akan/` either: the deny list covers them, so
14
+ quote the `/mcp` command that makes the change instead. The context files and skills named below you can edit.
15
15
 
16
16
  ## MCP servers
17
17
 
@@ -52,8 +52,17 @@ From the prompt:
52
52
 
53
53
  **A server added now is not reachable until the session reloads.** The tool allowlist is fixed when the
54
54
  session is created, so `add`, `remove`, `login` and `logout` all reload by themselves — a session that has
55
- said nothing yet has nothing on disk to reopen, and says so. If you edit `mcp.json` yourself, tell the user to
56
- run `/mcp reload`.
55
+ said nothing yet has nothing on disk to reopen, and says so. After the user edits `mcp.json` by hand, `/mcp
56
+ reload` makes it take effect.
57
+
58
+ **This repo's own servers start only once the user approves them.** At start, `akan code` lists what this
59
+ repo's `.akan/code/mcp.json` would run, and the extensions in `.akan/code/extensions/`, and asks once; a yes is
60
+ remembered in `~/.akan/code/trust.json` until that content changes, and is asked again after. An extensions
61
+ folder past 10,000 files or 64 MiB, or with a link that leads out of it, is asked about at every start. With
62
+ nobody at a terminal they are not started, and `/mcp` shows `not approved`. `--trust-workspace` (`-T`) starts
63
+ them for one run without asking. Servers in `~/.akan/code/mcp.json`, where `/mcp add` writes, need no approval;
64
+ `/mcp add --local` writes this repo's file and keeps the approval it had, so a server added while that file
65
+ waits for approval waits with it.
57
66
 
58
67
  `/mcp remove` on a name both files declare takes this repo's entry and leaves the home file's in force; its
59
68
  notice says so, and running it again removes that one too.
@@ -61,8 +70,9 @@ notice says so, and running it again removes that one too.
61
70
  Sign-in is discovered, not configured: the server's `401` names its resource metadata, that names the
62
71
  authorization server, and most hosted servers register a client on demand — so a url is usually the whole
63
72
  entry. For a provider that issues client ids by hand, add `"oauth": { "clientId": "…", "scope": "…" }`.
64
- Tokens are stored per server in `~/.akan/code/mcpAuth.json` at mode 0600, never in the repo, and refreshed
65
- silently — at connect when one has expired, and mid-session when a server refuses one, after which the call is
73
+ Tokens are stored in `~/.akan/code/mcpAuth.json` at mode 0600, never in the repo, bound to the server's origin
74
+ rather than its name — a server renamed in the file keeps its sign-in, and one moved to another url signs in
75
+ again. They are refreshed silently — at connect when one has expired, and mid-session when a server refuses one, after which the call is
66
76
  retried once. When a refresh cannot fix it, `/mcp` shows `sign-in needed` and that server's tools answer with
67
77
  `/mcp login <name>` until the user signs in; the sign-in reloads the session, which brings the server back.
68
78
 
@@ -85,19 +95,23 @@ Stored under `.akan/code/sessions` — project history, so a second checkout of
85
95
  ## Model, effort, keys
86
96
 
87
97
  - `/model <provider>/<id>`, e.g. `/model deepseek/deepseek-v4-pro`. `/effort <level>` for how hard it thinks.
98
+ A `task` sub-agent starts on the model the session is on at that moment.
88
99
  - A provider is authorized by **`<PROVIDER>_API_KEY`** — `DEEPSEEK_API_KEY`, `ANTHROPIC_API_KEY`. The
89
100
  workspace's own `.env` is read as well as the environment, and a key found there is used for the run and
90
101
  never copied anywhere.
91
- - Credentials and the model catalogue live in `~/.akan/code/` (`auth.json`, `models.json`). **`AKAN_CODE_HOME`
92
- moves the whole directory** — for a container with no home, or a checkout that wants its own.
102
+ - Credentials and the model catalogue live in `~/.akan/code/` (`auth.json`, `models.json`), mode 0700.
103
+ **`AKAN_CODE_HOME` moves the whole directory** — for a container with no home, or a checkout that wants its
104
+ own. It, `AKAN_CODE_CAN_PROMPT`, the `AKAN_CODE_PROXY_*` settings and `AKAN_CODE_RPC_TOKEN_FILE` are read
105
+ from the shell only: one a repo's `.env` names is ignored, with a warning at start.
93
106
 
94
107
  ## What the agent reads, and how to change it
95
108
 
96
109
  - Every `AGENTS.md` / `CLAUDE.md` from the workspace root down to the working directory, `.cursorrules`, and
97
110
  any `.cursor/rules/*.mdc` that applies unconditionally (`alwaysApply: true`, or no frontmatter at all).
98
111
  Edit those to change how this agent behaves in this repo — they are the files the editor beside it reads.
99
- - Skills: drop a `<name>/SKILL.md` into `.akan/code/skills`. Extensions: `.akan/code/extensions`. Neither is
100
- read from a home directory, so a session behaves the same in two checkouts.
112
+ - Skills: drop a `<name>/SKILL.md` into `.akan/code/skills`. Extensions: `.akan/code/extensions`, loaded once
113
+ the user approves them (see MCP servers). Neither is read from a home directory, so a session behaves the
114
+ same in two checkouts.
101
115
 
102
116
  ## Keys
103
117
 
@@ -123,8 +137,21 @@ akan code -i "<prompt>" open the session with the prompt already sent
123
137
  --profile local|pod|review|web what it may do (default local)
124
138
  --model <provider>/<id> --thinking, --json, --rpc, --resume <id>
125
139
  --compact-at <percent> compact once the window is this full (10, 20, … 100)
140
+ --trust-workspace start this repo's MCP servers and extensions without asking
141
+ --no-ask-network never ask before an unlisted host (local profile)
142
+ --rpc-listen unix:<path>|tcp:[host:]<port> serve the RPC wire on a socket instead of stdio
126
143
  ```
127
144
 
145
+ `local` runs as the user: it refuses known secret files and the home credential stores, and asks before it
146
+ fetches a page from a host off a short documentation list or a shell command that reaches the network
147
+ (`curl`, `ssh`, …) — refused outright where nobody can answer, as in a one-shot `akan code "<prompt>"`. It is
148
+ not a sandbox; `pod` in a container is.
149
+
150
+ A `tcp:` listener asks every connection to open with `{"auth":"<token>"}` on its first line; the token is a
151
+ fresh one in `~/.akan/code/rpc/<port>.token`, or the one in the file `AKAN_CODE_RPC_TOKEN_FILE` names.
152
+ `--no-rpc-token` drops it, and only a loopback address is bound unless `--rpc-bind-any` is given. A unix
153
+ socket is created owner-only and asks for no token.
154
+
128
155
  `review` reads and never writes, loads no project context files, and keeps no session on disk. `pod` has
129
156
  nobody in front of it: it cannot ask a question. MCP is off in both.
130
157
 
@@ -1,10 +1,43 @@
1
1
  ---
2
2
  name: akan-debug
3
- description: "Debug a running Akan app: where the dev-server log is, how to read it, and how to tell a runtime failure from a build one."
3
+ description: "Check and debug a running Akan app: akan preview for what a page shows and throws, where the dev-server log is, how to read it, and how to tell a runtime failure from a build one."
4
4
  ---
5
5
 
6
6
  # Akan debug
7
7
 
8
+ ## See the change run: `akan preview`
9
+
10
+ `akan preview <app> [paths]` opens pages of the app's dev server in a headless browser and prints, per page,
11
+ the HTTP status, the title, a slice of the visible text, the uncaught errors and rejections the page reported,
12
+ the server errors logged meanwhile, console errors and warnings, and layout smells. It exits 1 when a page failed.
13
+
14
+ - It uses the dev server that answers for this app and checkout. With none, it starts one for the run and
15
+ stops it after; `--keep` leaves it up for the next preview. It never starts a second server beside a
16
+ running one.
17
+ - Paths are comma-separated (`akan preview <app> /board,/board/new`) and get the default locale when they lack
18
+ one, unless the app sets `localePrefix: "as-needed"` and `--csr` is off.
19
+ `--changed` adds the pages your uncommitted page and layout edits reach. A dynamic route needs a concrete
20
+ path (`/memo/1`); `akan context --app <app>` lists every route with its params and search names.
21
+ - The viewport is a 390x844 phone; `--desktop` is 1280x800. `--csr` loads each page in the CSR boot the native
22
+ shells load. `--screenshot` saves a PNG per page under `local/apps/<app>/runtime/preview/`, for a model
23
+ that reads images. `--format json` gives the same report as data.
24
+
25
+ ### Which platforms that covers
26
+
27
+ - Every change: the server-rendered web page and, with `--csr`, the bundle the iOS, Android and desktop
28
+ shells run.
29
+ - A change to `native/`, `plugin/`, an `isNativeApp()` branch, `native` in `akan.config.ts` or an
30
+ `@akanjs/native` plugin also needs that shell: `akan start-ios`, `start-android` or `start-desktop` on top of
31
+ the running dev server. Errors thrown in the shell reach the same dev-server log as `ClientReport` lines
32
+ tagged with the platform and target. Capture the shell with `xcrun simctl io booted screenshot <file>.png` or
33
+ `adb exec-out screencap -p > <file>.png`. When you cannot run the shell, say so: "not checked on iOS".
34
+ - Real devices, memory, OTA updates and store builds are for whoever releases.
35
+
36
+ ### What goes in your answer
37
+
38
+ The verdict lines of the pages you changed (`ok /en/board 200 "Board" 1.4s`) and every finding you left
39
+ unfixed. "Not opened in a browser" is not an answer once `akan preview` can run.
40
+
8
41
  ## The log is a file, and that file is the whole session
9
42
 
10
43
  A supervised `akan start` writes **`local/apps/<app>/runtime/dev.log`** — no ANSI, nothing truncated, every
@@ -17,12 +50,14 @@ what survives in the scrollback is bordered and truncated to the pane.
17
50
  - `tail -n 200 local/apps/<app>/runtime/dev.log` after an edit, to see what the reload produced.
18
51
  - `akan logs <app>` tails a running server with `--level --grep --endpoint --trace --child --role --origin
19
52
  --since` and `--json`.
20
- - The dev server's URL is printed in that log, and the port is allocated rather than fixed — the log is the
21
- only source for it.
53
+ - The dev server's URL is printed in that log. A server `akan preview` started logs to
54
+ `local/apps/<app>/runtime/preview/server.log` instead.
55
+ - An uncaught error or unhandled rejection in a page reaches the server log as a `ClientReport` error line,
56
+ naming the page, the client (`ssr` or `csr`) and the native target.
22
57
 
23
58
  ## Which loop you are in
24
59
 
25
- - **A type or lint failure** is the `akan-validate` loop. `akan_verify`.
60
+ - **A type or lint failure** is the `akan-validate` loop: `akan verify`.
26
61
  - **A runtime or SSR failure** is the dev log. The page renders blank, an overlay appears, or a request
27
62
  500s — none of that shows up in a typecheck.
28
63
 
@@ -38,5 +73,5 @@ reads like its own level is not one.
38
73
 
39
74
  ## Before you conclude
40
75
 
41
- A change that renders is not a change that passed. Run `akan_verify`, and for a `.tsx` change check
42
- `akan quality ssr` too — a UI edit that moves markup into a client bundle typechecks perfectly.
76
+ A change that renders is not a change that passed. Run `akan verify`; for a `.tsx` change it also runs the
77
+ SSR check, since a UI edit that moves markup into a client bundle typechecks perfectly.
@@ -46,5 +46,4 @@ the package client path — `import { cnst, fetch, st, usePage } from "@apps/<ap
46
46
 
47
47
  ## Validate
48
48
 
49
- `akan_verify`, or `akan sync <app>` then `akan lint <app>` then `akan typecheck <app>`. Raw `tsc` misses the
50
- generated barrels and reports false errors.
49
+ `akan verify`. Raw `tsc` misses the generated barrels and reports false errors.
@@ -28,7 +28,8 @@ import path at all**, so keep throwing code out of them entirely rather than rea
28
28
 
29
29
  Best-effort code returns a sentinel (`null`, `undefined`, `{}`) instead of throwing. `try/catch` is rare and
30
30
  always converts an exception into a decision: a guard catches, warns, and returns `false`; an adapter
31
- catches, logs, and returns `null`. A store action never catches — let the framework toast the `Err`.
31
+ catches, logs, and returns `null`. A store action never catches — let the framework toast the `Err`, or list it in
32
+ the store's `static silent` when no screen waits on it and its caller handles the rejection.
32
33
  Client-side validation failure is `msg.error("<key>")` plus an early return, never a throw.
33
34
 
34
35
  **A failure another process reported travels as itself.** A server-to-server `fetch.<endpoint>(…, { origin })`
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: akan-mcp
3
+ description: "Expose an app to MCP clients: what /mcp publishes from guards, mcp: false curation, option.setMcp settings, OAuth, refusals, page prompts and MCP Apps, and the exposure test."
4
+ ---
5
+
6
+ # Akan MCP
7
+
8
+ Every app serves its signals as an MCP server on `POST /mcp`, mounted by default. **Exposure follows the guards**:
9
+ an endpoint with a real guard is published, one with none is refused, and so is a mutation guarded only by
10
+ `Public`. There is no per-endpoint opt-in.
11
+
12
+ ## Curation is not authorization
13
+
14
+ - `mcp: false` on an endpoint, or a `slice()` `mcp` map mirroring `guards`, keeps a perfectly guarded step (a UI
15
+ state-machine step a model would only call by mistake) off the shelf. It is an opt-out; everything else still
16
+ publishes.
17
+ - `guards: [Every, Person]` reserves an act for a person; `.with(AgentCall)` narrows what an agent's call may do.
18
+ - A guard's `static scope` decides whether a listing can hide the entry: `"account"` guards are evaluated per
19
+ caller, `"resource"` guards stay visible and stop the call.
20
+
21
+ ## Settings
22
+
23
+ - `option.setMcp({ … })` in the app's `lib/option.ts`, never `main.ts`; `AKAN_MCP=false` takes the surface off.
24
+ - `JWT_SECRET` is required outside `local`.
25
+ - Every published entry needs a `.desc()`; model-facing text is English.
26
+ - Never make a refusal message more helpful.
27
+
28
+ ## Pages for the host
29
+
30
+ - `page().prompt(name, description)` briefs a screen as an MCP prompt.
31
+ - `page().app(name, description, load)` publishes one component a host draws in the chat, as a read-only tool
32
+ whose `load` may only query.
33
+
34
+ ## Verify
35
+
36
+ - One exposure test per app in `srvkit/mcpExposure.signal.test.ts`: list the tools with a user token, pin the names
37
+ with `toEqual`, and assert the curated and admin-only ones are absent. A guard change fails it first.
38
+ - Then `akan verify <app>` and the boot log.
39
+
40
+ Full detail: `get_guideline` with `mcpRule`, or `transportRule` for guards and rate limits.
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: akan-page
3
+ description: "Add or change a route under page/: the one page()/layout() chain, typed params and search, guards, heads, loading and errors, overrides, and lib pages."
4
+ ---
5
+
6
+ # Akan pages
7
+
8
+ A route file exports one chain and nothing else. Every setting is a stage of it.
9
+
10
+ ```tsx
11
+ export default page()
12
+ .param("orgId", ID)
13
+ .guard(() => signedIn())
14
+ .render(({ orgId }) => {
15
+ const { l } = usePage();
16
+ const { taskInitInOrg } = fetch.initTaskInOrg(orgId);
17
+ return <Task.Zone.Card init={taskInitInOrg} prefix={`/org/${orgId}`} />;
18
+ });
19
+ ```
20
+
21
+ ## Steps
22
+
23
+ 1. Place the file: `<route>.tsx` or `<dir>/_index.tsx` serves the path, `_layout.tsx` nests a layout (`layout()`;
24
+ the root one is `rootLayout()`), `[id]` is a dynamic segment, `(group)` groups without a segment. `page/` holds
25
+ route modules only — no helpers, no `.ts` files.
26
+ 2. Name every `[x]` segment with `.param("x", Type)` and read a query key only with `.search("k", Type)`. Values
27
+ arrive typed; `lang` reaches every stage and is never declared.
28
+ 3. Gate with `.guard(() => signedIn())` on the `_layout.tsx` (a real redirect before anything renders), and repeat
29
+ a stricter guard on a page that needs more. The endpoint guards are still the real gate.
30
+ 4. In `.render()`: `usePage()`, then fetch, then return. Hand `fetch.init*` / `view*` / `edit*` fields to sections
31
+ unawaited; await only what the shell reads, in one `Promise.all`.
32
+ 5. `.head()` takes JSX (or a function returning it), never a metadata object. A canonical link comes only from the
33
+ page's own head.
34
+ 6. Re-skin an `akanjs/ui` component for a subtree with `_overrides.tsx`:
35
+ `export default override({ Slot: AppComponent })`, no logic.
36
+
37
+ ## Never
38
+
39
+ - A `fetch.<mutation>()` while rendering — it throws `base.error.mutationInRender`, because opening a link would
40
+ perform it. Send writes from a user action.
41
+ - `useState`, `useEffect`, comments, a named export beside the chain, or the legacy `export default async function`.
42
+ - Editing a lib page through its link in `page/(libs)/` — edit the lib's own `page/` source.
43
+
44
+ Full detail: `get_guideline` with `pageRule`, or `ssrRule` for what may render on the server.
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: akan-query
3
+ description: "Write filters, slices and list loading: query descriptors, init paging and loadMoreOf, soft removal and hookless writes, q.search text search, and the cascade direction."
4
+ ---
5
+
6
+ # Akan queries and slices
7
+
8
+ ## Filters and slices
9
+
10
+ - A filter in `<model>.document.ts` declares its args; a slice in `<model>.signal.ts` serves it to stores. A slice
11
+ `exec` returns a `query<Filter>(…)` descriptor — never a `list<Filter>` array — and you cannot chain sort or
12
+ limit on it.
13
+ - Page through the store: `initX(…, { sort, page, limit })`, then `st.do.loadMoreOf<Model>()` while
14
+ `hasMoreOf<Model>`. Never compare `count > page * limit`.
15
+ - Never ask for more than 500 rows: the server serves at most 500 per request and drops the rest silently.
16
+ A figure over the whole set belongs in an insight or an endpoint.
17
+ - The root slice takes a filter name and its args, never a raw query, and its `root:` guard is always `Admin`.
18
+ - No value is `q.empty`, never `q.missing`.
19
+ - A projection is bare on the facade (`pickById(id, { secret: true })`) and under `select` on a filter accessor —
20
+ the only way to read a `field.secret` value.
21
+
22
+ ## Writes and removal
23
+
24
+ - Removal is soft: reads see live rows unless `{ withRemoved: true }`. The one hard delete is `purge<Filter>` /
25
+ `purgeMany`, for data past retention.
26
+ - A filter's `remove*` / `update*`, `updateById` and `removeById` run no hooks and no cascade. A model with a
27
+ cascade or `_postRemove` removes through `remove<Model>(id)`.
28
+ - Load a set once (`load<Model>Many`, one filter query) and create one with `create<Model>Many(rows)` instead of one
29
+ call per item in a loop — lint fails a per-item service call an endpoint reaches. `create<Model>Many` runs every
30
+ row's `_preCreate` before writing any, so a check there does not see the batch's own rows.
31
+
32
+ ## Search and cascade
33
+
34
+ - `q.search(text, { prefix: true })` sits in a filter at an AND position, over fields with the `text` role. Blank
35
+ input matches nothing — never a passthrough. A search slice only over data safe to enumerate.
36
+ - `cascade` names a direction, and the wrong one loses data: `removeRef` on the owner's relation, `removeWith` on
37
+ the child's reference to its owner.
38
+
39
+ Full detail: `get_guideline` with `queryRule`, and `workspaceRecipes` for a worked slice.
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: akan-runtime
3
+ description: "Configure how an app runs: web surfaces, route prefixes, gateway vs solo, akan start and logs, the Docker image, native and desktop apps, database modes, ops, assets and the build pipeline."
4
+ ---
5
+
6
+ # Akan runtime
7
+
8
+ Runtime and build settings are declared in `akan.config.ts` and narrowed — never widened — by an env at boot.
9
+ Domain code reaches none of them.
10
+
11
+ ## Where each setting lives
12
+
13
+ | Want | Setting |
14
+ | --- | --- |
15
+ | API only, or no CSR bundle for native apps | `web: false`, `web: { csr: false }` |
16
+ | Move the API or websocket mount | `new AkanApp({ prefix, websocketPrefix })` in `main.ts`, mirrored in `api: { … }`; read with `getApiPrefix()` / `getWsPrefix()` |
17
+ | The production image | `docker: string \| { image, preRuns, postRuns, command }` |
18
+ | iOS, Android, desktop | the `native` section; `desktop: { server: true }` carries the server |
19
+ | SQLite, several databases, a cluster | `database: { modes: [...] }`, picked by `AKAN_DATABASE_MODE` |
20
+ | URLs without `/<locale>/`: one language, or paths fixed from outside (OAuth callback, QR code, mail link) | `i18n: { localePrefix: "as-needed" }` — the default locale's pages and `<Link>` hrefs drop the prefix, other locales keep it |
21
+
22
+ - `main.ts` imports `AkanApp` from `akanjs/server/akanApp`, never the barrel.
23
+ - Never write `/api` or `/ws` literally, and never pass a prefix as a prop.
24
+ - The generated image installs only `ca-certificates` and `tzdata`; add the rest in `preRuns`.
25
+ - An app running in two database modes avoids `q.raw`.
26
+
27
+ ## Running and logs
28
+
29
+ - `akan start <app>` (`a,b` or `all` for several) runs the dev server; `akan build <app>` checks the production
30
+ build. A supervised session writes `local/apps/<app>/runtime/dev.log` — hand over that path, not a paste of the
31
+ live view. Tail with `akan logs <app>`.
32
+ - Run a build without Docker with `cd dist/apps/<app> && bun main.js`: it boots with the env the Dockerfile sets,
33
+ read from `akan.env.json` beside it, and a variable you set yourself (`PORT=3000`) wins.
34
+ - Give every log sink a floor (`Logger.addSink(sink, { minLevel })`) and never log once per delivered record.
35
+
36
+ ## Native and desktop
37
+
38
+ - `akan start-desktop <app>`, `akan start-ios <app>`, `akan start-android <app>` load pages from a running
39
+ `akan start`; a desktop app carrying its server starts it itself.
40
+ - A desktop app's carried server listens on a loopback port any local program can call — guard it like a network
41
+ server. Read runtime files from `AKAN_APP_DIR`, and keep a user's secret with `NativeSecret`, never in a file.
42
+
43
+ Full detail: `get_guideline` with `runtimeRule`.