@akanjs/devkit 3.1.1-rc.9 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) 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 +30 -0
  8. package/akanConfig/akanConfig.ts +10 -0
  9. package/akanConfig/productionLockfile.test.ts +31 -0
  10. package/akanConfig/productionLockfile.ts +2 -0
  11. package/akanContext.ts +129 -4
  12. package/applicationBuildRunner.test.ts +17 -0
  13. package/applicationBuildRunner.ts +3 -1
  14. package/biome.base.json +4 -1
  15. package/codeAgent/agent/CodeAgent.ts +4 -0
  16. package/codeAgent/agent/SubagentPool.ts +5 -0
  17. package/codeAgent/agent/akanSystemPrompt.ts +1 -1
  18. package/codeAgent/agent/codeAgent.test.ts +41 -2
  19. package/codeAgent/feedback/PreviewFeedback.ts +2 -2
  20. package/codeAgent/feedback/VerifyFeedback.ts +3 -4
  21. package/codeAgent/index.ts +2 -2
  22. package/codeAgent/skills/akan-agent/SKILL.md +36 -0
  23. package/codeAgent/skills/akan-code/SKILL.md +1 -0
  24. package/codeAgent/skills/akan-debug/SKILL.md +40 -6
  25. package/codeAgent/skills/akan-deps/SKILL.md +1 -2
  26. package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
  27. package/codeAgent/skills/akan-page/SKILL.md +44 -0
  28. package/codeAgent/skills/akan-query/SKILL.md +38 -0
  29. package/codeAgent/skills/akan-runtime/SKILL.md +40 -0
  30. package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
  31. package/codeAgent/skills/akan-transport/SKILL.md +40 -0
  32. package/codeAgent/skills/akan-validate/SKILL.md +13 -11
  33. package/codeAgent/tools/AkanCli.ts +2 -1
  34. package/codeAgent/tools/AkanCodePlugins.ts +5 -1
  35. package/codeAgent/tools/AkanEditScope.ts +8 -3
  36. package/codeAgent/tools/AkanToolPack.ts +4 -10
  37. package/codeAgent/tools/AkanVerifier.ts +13 -31
  38. package/codeAgent/tools/akanCodePlugins.test.ts +60 -58
  39. package/commandDecorators/workspaceVersionCheck.test.ts +17 -7
  40. package/commandDecorators/workspaceVersionCheck.ts +4 -1
  41. package/commentDensityScanner.test.ts +121 -0
  42. package/commentDensityScanner.ts +101 -0
  43. package/componentSizeScanner.test.ts +36 -0
  44. package/componentSizeScanner.ts +52 -0
  45. package/componentWriteChainScanner.test.ts +137 -0
  46. package/componentWriteChainScanner.ts +104 -0
  47. package/conventionSet.test.ts +73 -0
  48. package/conventionSet.ts +67 -0
  49. package/csrE2e/ssrRegistry.e2e.test.ts +6 -1
  50. package/deprecatedCommentScanner.test.ts +54 -0
  51. package/deprecatedCommentScanner.ts +30 -0
  52. package/endpointReach.test.ts +58 -0
  53. package/endpointReach.ts +80 -0
  54. package/executors.test.ts +0 -4
  55. package/executors.ts +18 -2
  56. package/index.ts +1 -0
  57. package/integration/devStability.integration.test.ts +9 -5
  58. package/kindBranchScanner.test.ts +89 -0
  59. package/kindBranchScanner.ts +158 -0
  60. package/lint/__fixtures__/no-oversized-list-limit/bad.tsx +18 -0
  61. package/lint/__fixtures__/no-oversized-list-limit/good.tsx +23 -0
  62. package/lint/no-oversized-list-limit.grit +73 -0
  63. package/linter.test.ts +12 -0
  64. package/linter.ts +35 -12
  65. package/maintainabilityBaseline.test.ts +280 -0
  66. package/maintainabilityBaseline.ts +220 -0
  67. package/maintainabilityContract.test.ts +846 -0
  68. package/maintainabilityContract.ts +404 -0
  69. package/maintainabilityExceptionScanner.test.ts +90 -0
  70. package/maintainabilityExceptionScanner.ts +85 -0
  71. package/maintainabilityLimits.ts +60 -0
  72. package/maintainabilityReport.test.ts +130 -0
  73. package/maintainabilityReport.ts +153 -0
  74. package/mobile/desktopRelease.test.ts +1 -1
  75. package/mobile/desktopRelease.ts +3 -1
  76. package/package.json +3 -2
  77. package/platformTest/LinuxDockerTarget.ts +3 -0
  78. package/{codeAgent/feedback → preview}/PreviewChrome.ts +13 -7
  79. package/preview/PreviewView.ts +219 -0
  80. package/preview/index.ts +2 -0
  81. package/preview/previewView.test.ts +76 -0
  82. package/prompter.ts +7 -0
  83. package/qualityScanner.test.ts +75 -0
  84. package/qualityScanner.ts +17 -1
  85. package/routeSourceValidator.test.ts +11 -2
  86. package/routeSourceValidator.ts +14 -2
  87. package/serviceLoopCallScanner.test.ts +56 -0
  88. package/serviceLoopCallScanner.ts +122 -0
  89. package/signalMutationIndex.test.ts +15 -0
  90. package/signalMutationIndex.ts +1 -1
  91. package/sourceComments.test.ts +45 -0
  92. package/sourceComments.ts +70 -0
  93. package/storeAccessScanner.test.ts +84 -0
  94. package/storeAccessScanner.ts +71 -0
  95. package/storeSource.ts +159 -0
  96. package/storeWriteScanner.test.ts +292 -0
  97. package/storeWriteScanner.ts +50 -0
  98. package/testOnlyExportScanner.test.ts +83 -0
  99. package/testOnlyExportScanner.ts +124 -0
  100. package/typeChecker.ts +32 -0
  101. package/typecheck/projectTypecheck.test.ts +18 -0
  102. package/typecheck/projectTypecheck.ts +29 -5
  103. package/typecheck/typecheck.proc.ts +6 -0
  104. package/typecheck/typecheckFailure.ts +13 -0
  105. package/types.ts +1 -0
  106. package/workflow/artifacts.ts +16 -2
  107. package/workflow/executor.ts +22 -7
  108. package/workflow/guardCatalog.test.ts +59 -0
  109. package/workflow/guardCatalog.ts +108 -0
  110. package/workflow/index.ts +4 -0
  111. package/workflow/interactionScaffold.ts +122 -0
  112. package/workflow/mutationScaffold.ts +519 -0
  113. package/workflow/mutationSource.test.ts +108 -0
  114. package/workflow/mutationSource.ts +219 -0
  115. package/workflow/plan.ts +54 -0
  116. package/workflow/render.ts +19 -4
  117. package/workflow/source.test.ts +185 -0
  118. package/workflow/source.ts +151 -24
  119. package/workflow/types.ts +18 -1
  120. package/workspaceLayout.ts +2 -0
  121. package/writePath.test.ts +109 -0
  122. package/writePath.ts +219 -0
  123. package/writePlacementScanner.test.ts +77 -0
  124. package/writePlacementScanner.ts +50 -0
  125. package/writingActionIndex.test.ts +99 -0
  126. package/writingActionIndex.ts +63 -0
  127. package/codeAgent/feedback/PreviewView.ts +0 -95
  128. /package/{codeAgent/feedback → preview}/previewChrome.test.ts +0 -0
package/akanContext.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { capitalize, isRecord } from "akanjs/common";
3
+ import { capitalize, isRecord, parseRouteModuleKey } from "akanjs/common";
4
4
  import { extractBlockVersion, readDevkitVersion } from "./agentsIndex";
5
5
  import { AppExecutor, LibExecutor, type SysExecutor, type WorkspaceExecutor } from "./executors";
6
6
  import { FileSys } from "./fileSys";
@@ -48,12 +48,25 @@ export interface AkanModuleContext {
48
48
  files: string[];
49
49
  }
50
50
 
51
+ export interface AkanRouteContext {
52
+ /** The URL pattern without the `/:lang` segment every route starts with. */
53
+ path: string;
54
+ file: string;
55
+ search: string[];
56
+ /** The page or a layout above it calls `.guard()`. */
57
+ guarded: boolean;
58
+ devOnly: boolean;
59
+ }
60
+
51
61
  export interface AkanSysContext {
52
62
  type: "app" | "lib";
53
63
  name: string;
54
64
  path: string;
55
65
  hasConfig: boolean;
56
66
  modules: AkanModuleContext[];
67
+ /** Listed for the app `--app` names only; reading them parses every route file. */
68
+ routes?: AkanRouteContext[];
69
+ routeError?: string;
57
70
  }
58
71
 
59
72
  export interface AkanPackageContext {
@@ -280,6 +293,7 @@ const generatedFiles = [
280
293
  ];
281
294
 
282
295
  const validationCommands = [
296
+ "akan verify [<app-or-lib-or-pkg>]",
283
297
  "akan sync <app-or-lib>",
284
298
  "akan lint <app-or-lib-or-pkg>",
285
299
  "akan typecheck <app-or-lib>",
@@ -294,7 +308,7 @@ const unknownGeneratedFilesFreshness: GeneratedFilesFreshness = {
294
308
  status: "unknown" as const,
295
309
  message: "Run sync before validation so generated Akan files match the current source conventions.",
296
310
  refreshCommand: "akan sync <app-or-lib>",
297
- verifyingCommands: ["akan lint <app-or-lib-or-pkg>", "akan build <app-name>"],
311
+ verifyingCommands: ["akan verify <app-or-lib>"],
298
312
  };
299
313
 
300
314
  const repairAction = (
@@ -454,7 +468,7 @@ const generatedFreshnessFromStates = async (workspace: WorkspaceExecutor): Promi
454
468
  ? `Generated files were refreshed for ${lastFresh.target} at ${lastFresh.lastSyncedAt}.`
455
469
  : "Generated sync state exists, but the last recorded repair did not pass.",
456
470
  refreshCommand: "akan sync <app-or-lib>",
457
- verifyingCommands: ["akan lint <app-or-lib-or-pkg>", "akan build <app-name>"],
471
+ verifyingCommands: ["akan verify <app-or-lib>"],
458
472
  targets,
459
473
  };
460
474
  };
@@ -556,6 +570,34 @@ const getSysModules = async (
556
570
  return modules.sort((a, b) => `${a.sysName}:${a.path}`.localeCompare(`${b.sysName}:${b.path}`));
557
571
  };
558
572
 
573
+ const getAppRoutes = async (app: AppExecutor, workspaceRoot: string): Promise<AkanRouteContext[]> => {
574
+ const sources = await app.getRouteSources();
575
+ const guardedLayouts = new Set<string>();
576
+ const pages: { source: (typeof sources)[number]; parsed: ReturnType<typeof parseRouteModuleKey> }[] = [];
577
+ for (const source of sources) {
578
+ const parsed = parseRouteModuleKey(source.key);
579
+ if (parsed.kind === "layout" && source.info.chain?.guarded) guardedLayouts.add(parsed.routeSegments.join("/"));
580
+ else if (parsed.kind === "page") pages.push({ source, parsed });
581
+ }
582
+ return pages
583
+ .map(({ source, parsed }) => {
584
+ const segments = parsed.routeSegments;
585
+ //? The layouts that wrap a page, as the route seed index walks them: an `_index` sits inside its own folder.
586
+ const reach = parsed.leaf === "_index" ? segments.length : segments.length - 1;
587
+ const underGuard = Array.from({ length: reach + 1 }, (_, idx) => segments.slice(0, idx).join("/")).some(
588
+ (prefix) => guardedLayouts.has(prefix),
589
+ );
590
+ return {
591
+ path: parsed.pattern.replace(/^\/:lang/, "") || "/",
592
+ file: path.relative(workspaceRoot, source.absPath).replaceAll(path.sep, "/"),
593
+ search: source.info.chain?.search ?? [],
594
+ guarded: !!source.info.chain?.guarded || underGuard,
595
+ devOnly: source.info.devOnly,
596
+ };
597
+ })
598
+ .sort((a, b) => a.path.localeCompare(b.path));
599
+ };
600
+
559
601
  const getSysContext = async (
560
602
  workspace: WorkspaceExecutor,
561
603
  type: "app" | "lib",
@@ -563,7 +605,7 @@ const getSysContext = async (
563
605
  options: AkanContextOptions,
564
606
  ): Promise<AkanSysContext> => {
565
607
  const sys = type === "app" ? AppExecutor.from(workspace, name) : LibExecutor.from(workspace, name);
566
- return {
608
+ const context: AkanSysContext = {
567
609
  type,
568
610
  name,
569
611
  path: `${type}s/${name}`,
@@ -573,6 +615,12 @@ const getSysContext = async (
573
615
  module: options.module,
574
616
  }),
575
617
  };
618
+ if (sys instanceof AppExecutor && options.app === name)
619
+ await getAppRoutes(sys, workspace.workspaceRoot).then(
620
+ (routes) => (context.routes = routes),
621
+ (error: unknown) => (context.routeError = error instanceof Error ? error.message : String(error)),
622
+ );
623
+ return context;
576
624
  };
577
625
 
578
626
  export class AkanContextAnalyzer {
@@ -632,6 +680,42 @@ export class AkanContextAnalyzer {
632
680
  };
633
681
  }
634
682
 
683
+ //? Codex joins every AGENTS.md from the repo root down to its folder and drops what passes 32 KiB (`project_doc_max_bytes`).
684
+ static async #agentGuideOversize(workspace: WorkspaceExecutor, sysPaths: string[]) {
685
+ const sizeOf = async (file: string) => {
686
+ const content = await safeReadText(path.join(workspace.workspaceRoot, file));
687
+ return { file, bytes: content === null ? 0 : Buffer.byteLength(content) };
688
+ };
689
+ const root = await sizeOf("AGENTS.md");
690
+ if (root.bytes === 0) return null;
691
+ const scoped = await Promise.all(sysPaths.map((sysPath) => sizeOf(`${sysPath}/AGENTS.md`)));
692
+ const largest = scoped.reduce((max, entry) => (entry.bytes > max.bytes ? entry : max), { file: "", bytes: 0 });
693
+ const total = root.bytes + largest.bytes;
694
+ if (total <= 32 * 1024 * 0.8) return null;
695
+ const subject = largest.bytes ? `AGENTS.md and ${largest.file} add up to` : "AGENTS.md is";
696
+ return `${subject} ${Math.ceil(total / 1024)} KiB of the 32 KiB Codex reads — run \`akan agent install\`, then shorten the text outside the akan:agent markers or move it into a skill.`;
697
+ }
698
+
699
+ static async #agentGuideLeftovers(workspace: WorkspaceExecutor) {
700
+ const root = workspace.workspaceRoot;
701
+ const guide = (await safeReadText(path.join(root, "AGENTS.md"))) ?? "";
702
+ const leftovers: { path: string; message: string }[] = [];
703
+ for (const doc of ["docs/AI-DEVELOPMENT.md", "docs/GENERATED.md"]) {
704
+ const pointer = guide.includes(doc);
705
+ if ((await safeReadText(path.join(root, doc))) !== null)
706
+ leftovers.push({
707
+ path: doc,
708
+ message: `${doc} is an older template's copy of the agent guide, and nothing reads it now — delete it${pointer ? " and the AGENTS.md sentence that points to it" : ""}.`,
709
+ });
710
+ else if (pointer)
711
+ leftovers.push({
712
+ path: "AGENTS.md",
713
+ message: `AGENTS.md points to ${doc}, which is gone — delete that sentence.`,
714
+ });
715
+ }
716
+ return leftovers;
717
+ }
718
+
635
719
  //? Older templates wrote these for Next.js: nothing installs or reads them, and editors warn about the plugin.
636
720
  static async #configLeftovers(workspace: WorkspaceExecutor, sysPaths: string[]) {
637
721
  const leftovers: { path: string; message: string }[] = [];
@@ -724,6 +808,11 @@ export class AkanContextAnalyzer {
724
808
  repairAction("generated", "akan agent install agents-md", agentDrift.hint, true),
725
809
  );
726
810
  const sysPaths = [...context.apps, ...context.libs].map((sys) => sys.path);
811
+ const oversize = await AkanContextAnalyzer.#agentGuideOversize(workspace, sysPaths);
812
+ if (oversize)
813
+ diagnostics.push({ severity: "warning", code: "agent-guide-oversized", path: "AGENTS.md", message: oversize });
814
+ for (const leftover of await AkanContextAnalyzer.#agentGuideLeftovers(workspace))
815
+ diagnostics.push({ severity: "warning", code: "agent-guide-leftover", ...leftover });
727
816
  for (const leftover of await AkanContextAnalyzer.#configLeftovers(workspace, sysPaths))
728
817
  diagnostics.push({ severity: "warning", code: "config-leftover", ...leftover });
729
818
 
@@ -944,6 +1033,12 @@ export class AkanContextAnalyzer {
944
1033
  ...context.pkgs.map((pkg) => `- ${pkg.name}${pkg.version ? ` (${pkg.version})` : ""}`),
945
1034
  );
946
1035
 
1036
+ for (const app of context.apps) {
1037
+ if (app.routeError) lines.push("", `## Routes: ${app.name}`, `- Not read: ${app.routeError}`);
1038
+ else if (app.routes)
1039
+ lines.push("", `## Routes: ${app.name}`, ...AkanContextAnalyzer.#routeLines(app.name, app.routes));
1040
+ }
1041
+
947
1042
  const modules = AkanContextAnalyzer.findModules(context, moduleName);
948
1043
  lines.push("", "## Modules");
949
1044
  for (const module of modules) {
@@ -958,4 +1053,34 @@ export class AkanContextAnalyzer {
958
1053
  lines.push("", "## Validation", ...context.validationCommands.map((command) => `- \`${command}\``));
959
1054
  return `${lines.join("\n")}\n`;
960
1055
  }
1056
+
1057
+ static readonly routeListLimit = 60;
1058
+
1059
+ //? Past the limit the biggest top folders fold into one line each: a docs section alone would fill the context.
1060
+ static #routeLines(appName: string, routes: AkanRouteContext[]) {
1061
+ const folderOf = (route: AkanRouteContext) => route.file.split("/").slice(0, 4).join("/");
1062
+ const groups = Map.groupBy(routes, folderOf);
1063
+ const folded = new Set<string>();
1064
+ let listed = routes.length;
1065
+ for (const [folder, members] of [...groups].sort((a, b) => b[1].length - a[1].length)) {
1066
+ if (listed <= AkanContextAnalyzer.routeListLimit) break;
1067
+ folded.add(folder);
1068
+ listed -= members.length;
1069
+ }
1070
+ const lines = [`Paths omit the \`/:lang\` prefix; \`akan preview ${appName} <path>\` opens one.`];
1071
+ for (const route of routes) {
1072
+ if (folded.has(folderOf(route))) continue;
1073
+ const search = route.search.length ? ` ?${route.search.join("&")}` : "";
1074
+ const marks = `${route.guarded ? " (guarded)" : ""}${route.devOnly ? " (dev only)" : ""}`;
1075
+ lines.push(`- ${route.path}${search}${marks} — ${route.file}`);
1076
+ }
1077
+ for (const folder of folded) {
1078
+ const members = groups.get(folder) ?? [];
1079
+ const examples = members.slice(0, 3).map((route) => route.path);
1080
+ lines.push(
1081
+ `- ${folder}/: ${members.length} routes folded (${examples.join(", ")}, …); \`--format json\` lists every route`,
1082
+ );
1083
+ }
1084
+ return lines;
1085
+ }
961
1086
  }
@@ -9,6 +9,7 @@ import type { App } from "./commandDecorators";
9
9
  import { AppExecutor, Executor } from "./executors";
10
10
  import { bundleDefine } from "./frontendBuild/bundleDefine";
11
11
  import { createTempApp } from "./testHelpers";
12
+ import { ProjectTypecheck } from "./typecheck/projectTypecheck";
12
13
 
13
14
  describe("ApplicationBuildRunner", () => {
14
15
  test("externalizes Akan optional backend dependencies", () => {
@@ -249,4 +250,20 @@ console.info(new SampleService().name);
249
250
  await rm(root, { recursive: true, force: true });
250
251
  }
251
252
  });
253
+
254
+ test("akan typecheck names the script/ files nothing imports, and a build's typecheck leaves them out", async () => {
255
+ const { root, app } = await createTempApp("portal");
256
+ const run = spyOn(ProjectTypecheck.prototype, "run").mockImplementation(async () => "");
257
+ try {
258
+ const runner = new ApplicationBuildRunner(app);
259
+ await runner.typecheck({ tests: true });
260
+ await runner.typecheck();
261
+ const [checked, built] = run.mock.calls.map(([entries]) => entries);
262
+ expect(checked).toContain("../../script/**/*.ts");
263
+ expect(built).not.toContain("../../script/**/*.ts");
264
+ } finally {
265
+ run.mockRestore();
266
+ await rm(root, { recursive: true, force: true });
267
+ }
268
+ });
252
269
  });
@@ -25,8 +25,9 @@ import type { BaseDevEnv, PackageJson } from "./types";
25
25
  export interface TypecheckOptions {
26
26
  clean?: boolean;
27
27
  incremental?: boolean;
28
- /** The app's own tests and specs too (`akan typecheck`); a build leaves them out. */
28
+ /** The app's own tests, specs and `script/` files too (`akan typecheck`); a build leaves them out. */
29
29
  tests?: boolean;
30
+ report?: "pretty" | "records";
30
31
  }
31
32
 
32
33
  export type BuildPhaseId = "prepare" | "typecheck" | "backend" | "ssr" | "csr" | "assets" | "compress" | "metadata";
@@ -205,6 +206,7 @@ export class ApplicationBuildRunner {
205
206
  "../../page/**/*.ts",
206
207
  "../../page/**/*.tsx",
207
208
  ...libPageIncludes,
209
+ ...(options.tests ? ["../../script/**/*.ts"] : []),
208
210
  "../../../../pkgs/akanjs/*/types/**/*.d.ts",
209
211
  ],
210
212
  options,
package/biome.base.json CHANGED
@@ -211,7 +211,10 @@
211
211
  "!**/*.spec.ts",
212
212
  "!**/*.spec.tsx"
213
213
  ],
214
- "plugins": ["./node_modules/@akanjs/devkit/lint/no-init-fetch-in-client.grit"]
214
+ "plugins": [
215
+ "./node_modules/@akanjs/devkit/lint/no-init-fetch-in-client.grit",
216
+ "./node_modules/@akanjs/devkit/lint/no-oversized-list-limit.grit"
217
+ ]
215
218
  },
216
219
  {
217
220
  "includes": [
@@ -114,6 +114,10 @@ export class CodeAgent {
114
114
  profile: options.profile,
115
115
  apps: options.apps ?? [],
116
116
  canSeeImages: akanCodeModelSupportsImages(model),
117
+ model: () => {
118
+ const current = agent.#session?.model;
119
+ return current ? { provider: current.provider, id: current.id } : undefined;
120
+ },
117
121
  deepseek: await DeepSeekWeb.fromRuntime(modelRuntime, !!proxy),
118
122
  depth: options.depth ?? 0,
119
123
  ...(options.subagentSpend ? { subagentSpend: options.subagentSpend } : {}),
@@ -2,11 +2,14 @@ import type { ExtensionAPI, InlineExtension } from "@earendil-works/pi-coding-ag
2
2
  import { type CodeAgentProfile, type CodeAgentSubagent, codeAgentReadOnlyBuiltins } from "akanjs/common";
3
3
  import { Type } from "typebox";
4
4
  import type { Workspace } from "../../commandDecorators";
5
+ import type { CodeAgentModelRef } from "./akanCodeModel";
5
6
 
6
7
  export interface SubagentPoolOptions {
7
8
  workspace: Workspace;
8
9
  cwd: string;
9
10
  parent: CodeAgentProfile;
11
+ /** The parent's model, read when a task starts: a child runs on the model the parent runs on now. */
12
+ model?: () => CodeAgentModelRef | undefined;
10
13
  depth: number;
11
14
  /** What the tree above this pool has spent and has running; a root pool starts its own. */
12
15
  spend?: SubagentSpend;
@@ -164,10 +167,12 @@ export class SubagentPool {
164
167
  const { CodeAgent } = await import("./CodeAgent");
165
168
  const budget = this.#options.parent.tools.subagent;
166
169
  const child = SubagentPool.childOf(this.#options.parent, kind);
170
+ const model = this.#options.model?.();
167
171
  const agent = await CodeAgent.create({
168
172
  workspace: this.#options.workspace,
169
173
  cwd: this.#options.cwd,
170
174
  profile: child,
175
+ ...(model ? { model } : {}),
171
176
  apps: [],
172
177
  depth: this.#options.depth + 1,
173
178
  subagentSpend: this.#spend,
@@ -13,7 +13,7 @@ export const akanSystemPrompt = (profile: CodeAgentProfile) => {
13
13
  ];
14
14
  if (profile.tools.builtin.includes("write"))
15
15
  lines.push(
16
- "- After editing, call `akan_verify`. It runs sync, lint, typecheck and the SSR check over what the working tree changed. A turn that edits and does not verify is not finished.",
16
+ "- After editing, call `akan_verify` once. It runs sync, lint, typecheck, a fast build and the SSR check over what the working tree changed; never run `akan lint`, `akan typecheck` or `akan build` separately. A turn that edits and does not verify is not finished.",
17
17
  );
18
18
  else
19
19
  lines.push(
@@ -6,7 +6,7 @@ import { type CodeAgentEvent, type CodeAgentProfile, codeAgentPresets } from "ak
6
6
  import { WorkspaceExecutor } from "../../executors";
7
7
  import { isolateEnv, tempDirs } from "../../testHelpers";
8
8
  import { AkanCodePlugins } from "../tools/AkanCodePlugins";
9
- import { akanCodeDefaultModel } from "./akanCodeModel";
9
+ import { akanCodeDefaultModel, type CodeAgentModelRef } from "./akanCodeModel";
10
10
  import { akanCodePaths } from "./akanCodePaths";
11
11
  import { CodeAgent, type CodeAgentOptions } from "./CodeAgent";
12
12
  import { CodeAgentSuspended } from "./CodeAgentSuspended";
@@ -37,11 +37,14 @@ const usageChunk = (promptTokens: number) =>
37
37
  let llm: ReturnType<typeof Bun.serve>;
38
38
  let llmCalls = 0;
39
39
  let reportedTokens = 0;
40
+ const requestedModels: string[] = [];
40
41
  beforeAll(() => {
41
42
  llm = Bun.serve({
42
43
  port: 0,
43
- fetch: () => {
44
+ fetch: async (request) => {
44
45
  llmCalls += 1;
46
+ const body = (await request.json().catch(() => ({}))) as { model?: string };
47
+ requestedModels.push(body.model ?? "");
45
48
  const usage = reportedTokens ? usageChunk(reportedTokens) : "";
46
49
  return new Response(
47
50
  `${chunk({ role: "assistant", content: "ok" })}${chunk({}, "stop")}${usage}data: [DONE]\n\n`,
@@ -160,6 +163,42 @@ describe("CodeAgent", () => {
160
163
  spy.mockRestore();
161
164
  }
162
165
  });
166
+
167
+ test("a task child sends its requests on the parent's model, and on the one the parent switches to", async () => {
168
+ const root = await makeTempRoot();
169
+ const build = AkanCodePlugins.build;
170
+ let parentModel: (() => CodeAgentModelRef | undefined) | undefined;
171
+ const spy = spyOn(AkanCodePlugins, "build").mockImplementation(async (options) => {
172
+ parentModel = options.model;
173
+ return await build.call(AkanCodePlugins, options);
174
+ });
175
+ const agent = await createAgent(root, { model: { provider: "deepseek", id: "deepseek-v4-pro" } });
176
+ spy.mockRestore();
177
+ const profile = quietProfile(root);
178
+ const pool = SubagentPool.extensionFor({
179
+ workspace: new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" }),
180
+ cwd: root,
181
+ parent: { ...profile, tools: { ...profile.tools, subagent: { maxDepth: 2, maxConcurrent: 3, budget: 100_000 } } },
182
+ ...(parentModel ? { model: parentModel } : {}),
183
+ depth: 0,
184
+ });
185
+ let execute: ((id: string, params: never) => Promise<unknown>) | undefined;
186
+ if (pool && "factory" in pool)
187
+ pool.factory({
188
+ registerTool: (tool: { execute: typeof execute }) => {
189
+ execute = tool.execute;
190
+ },
191
+ } as never);
192
+ try {
193
+ requestedModels.length = 0;
194
+ await execute?.("c1", { description: "d", prompt: "p" } as never);
195
+ await agent.setModel(akanCodeDefaultModel);
196
+ await execute?.("c2", { description: "d", prompt: "p" } as never);
197
+ expect(requestedModels).toEqual(["deepseek-v4-pro", akanCodeDefaultModel.id]);
198
+ } finally {
199
+ agent.dispose();
200
+ }
201
+ });
163
202
  });
164
203
 
165
204
  describe("a sub-agent its parent stops before the sub-agent's run is live", () => {
@@ -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,36 @@
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`.
18
+ 3. The LLM is `option.setLlm({ apiKey, model, host })`, never env.
19
+
20
+ ## Publish actions
21
+
22
+ - Declare the tool beside the control that already does it, and hand the result to the control:
23
+ `st.tool("x").desc("…").arg("id", ID).exec((id) => st.do.removeX(id))`. One handler serves the person and the
24
+ agent; `.desc()` is required and first.
25
+ - `.card(({ submit, cancel }) => <Form … />)` parks the call in the chat for an answer that is the user's to give.
26
+ - Store-driven forms publish themselves: `onChange={st.do.setFieldOnX}` by reference emits the `data-akan-action`
27
+ the agent drives. A pass-through arrow publishes nothing and fails lint.
28
+ - Every `st.tool(…).exec()` / `.card()` is a hook: never behind a condition.
29
+
30
+ ## Reads and guidance
31
+
32
+ - `st.use.x({ agent: false })` hides a key; `hidden`, `secret` and `visual` fields are stripped from every read.
33
+ - A tool value is capped at 20,000 characters.
34
+ - Route guidance is `<Agent.Guide instructions="…" />`; model-facing text is English.
35
+
36
+ Full detail: `get_guideline` with `agentRule`.
@@ -85,6 +85,7 @@ Stored under `.akan/code/sessions` — project history, so a second checkout of
85
85
  ## Model, effort, keys
86
86
 
87
87
  - `/model <provider>/<id>`, e.g. `/model deepseek/deepseek-v4-pro`. `/effort <level>` for how hard it thinks.
88
+ A `task` sub-agent starts on the model the session is on at that moment.
88
89
  - A provider is authorized by **`<PROVIDER>_API_KEY`** — `DEEPSEEK_API_KEY`, `ANTHROPIC_API_KEY`. The
89
90
  workspace's own `.env` is read as well as the environment, and a key found there is used for the run and
90
91
  never copied anywhere.
@@ -1,10 +1,42 @@
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 and get the default locale when they lack one: `akan preview <app> /board,/board/new`.
18
+ `--changed` adds the pages your uncommitted page and layout edits reach. A dynamic route needs a concrete
19
+ path (`/memo/1`); `akan context --app <app>` lists every route with its params and search names.
20
+ - The viewport is a 390x844 phone; `--desktop` is 1280x800. `--csr` loads each page in the CSR boot the native
21
+ shells load. `--screenshot` saves a PNG per page under `local/apps/<app>/runtime/preview/`, for a model
22
+ that reads images. `--format json` gives the same report as data.
23
+
24
+ ### Which platforms that covers
25
+
26
+ - Every change: the server-rendered web page and, with `--csr`, the bundle the iOS, Android and desktop
27
+ shells run.
28
+ - A change to `native/`, `plugin/`, an `isNativeApp()` branch, `native` in `akan.config.ts` or an
29
+ `@akanjs/native` plugin also needs that shell: `akan start-ios`, `start-android` or `start-desktop` on top of
30
+ the running dev server. Errors thrown in the shell reach the same dev-server log as `ClientReport` lines
31
+ tagged with the platform and target. Capture the shell with `xcrun simctl io booted screenshot <file>.png` or
32
+ `adb exec-out screencap -p > <file>.png`. When you cannot run the shell, say so: "not checked on iOS".
33
+ - Real devices, memory, OTA updates and store builds are for whoever releases.
34
+
35
+ ### What goes in your answer
36
+
37
+ The verdict lines of the pages you changed (`ok /en/board 200 "Board" 1.4s`) and every finding you left
38
+ unfixed. "Not opened in a browser" is not an answer once `akan preview` can run.
39
+
8
40
  ## The log is a file, and that file is the whole session
9
41
 
10
42
  A supervised `akan start` writes **`local/apps/<app>/runtime/dev.log`** — no ANSI, nothing truncated, every
@@ -17,12 +49,14 @@ what survives in the scrollback is bordered and truncated to the pane.
17
49
  - `tail -n 200 local/apps/<app>/runtime/dev.log` after an edit, to see what the reload produced.
18
50
  - `akan logs <app>` tails a running server with `--level --grep --endpoint --trace --child --role --origin
19
51
  --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.
52
+ - The dev server's URL is printed in that log. A server `akan preview` started logs to
53
+ `local/apps/<app>/runtime/preview/server.log` instead.
54
+ - An uncaught error or unhandled rejection in a page reaches the server log as a `ClientReport` error line,
55
+ naming the page, the client (`ssr` or `csr`) and the native target.
22
56
 
23
57
  ## Which loop you are in
24
58
 
25
- - **A type or lint failure** is the `akan-validate` loop. `akan_verify`.
59
+ - **A type or lint failure** is the `akan-validate` loop: `akan verify`.
26
60
  - **A runtime or SSR failure** is the dev log. The page renders blank, an overlay appears, or a request
27
61
  500s — none of that shows up in a typecheck.
28
62
 
@@ -38,5 +72,5 @@ reads like its own level is not one.
38
72
 
39
73
  ## Before you conclude
40
74
 
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.
75
+ A change that renders is not a change that passed. Run `akan verify`; for a `.tsx` change it also runs the
76
+ 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.
@@ -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.