@akanjs/devkit 3.1.1-rc.8 → 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.
- package/CHANGELOG.md +117 -0
- package/agentAssets.test.ts +65 -0
- package/agentAssets.ts +166 -0
- package/agentsIndex.ts +4 -4
- package/akanApp/akanApp.host.test.ts +12 -0
- package/akanApp/akanApp.host.ts +12 -0
- package/akanConfig/akanConfig.test.ts +30 -0
- package/akanConfig/akanConfig.ts +10 -0
- package/akanConfig/productionLockfile.test.ts +31 -0
- package/akanConfig/productionLockfile.ts +2 -0
- package/akanContext.ts +129 -4
- package/applicationBuildRunner.test.ts +17 -0
- package/applicationBuildRunner.ts +3 -1
- package/biome.base.json +4 -1
- package/cloud/cloudUpdates.ts +3 -2
- package/codeAgent/agent/CodeAgent.ts +4 -0
- package/codeAgent/agent/SubagentPool.ts +5 -0
- package/codeAgent/agent/akanSystemPrompt.ts +1 -1
- package/codeAgent/agent/codeAgent.test.ts +41 -2
- package/codeAgent/feedback/PreviewFeedback.ts +2 -2
- package/codeAgent/feedback/VerifyFeedback.ts +3 -4
- package/codeAgent/index.ts +2 -2
- package/codeAgent/skills/akan-agent/SKILL.md +36 -0
- package/codeAgent/skills/akan-code/SKILL.md +1 -0
- package/codeAgent/skills/akan-debug/SKILL.md +40 -6
- package/codeAgent/skills/akan-deps/SKILL.md +1 -2
- package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
- package/codeAgent/skills/akan-page/SKILL.md +44 -0
- package/codeAgent/skills/akan-query/SKILL.md +38 -0
- package/codeAgent/skills/akan-runtime/SKILL.md +40 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
- package/codeAgent/skills/akan-transport/SKILL.md +40 -0
- package/codeAgent/skills/akan-validate/SKILL.md +13 -11
- package/codeAgent/tools/AkanCli.ts +2 -1
- package/codeAgent/tools/AkanCodePlugins.ts +5 -1
- package/codeAgent/tools/AkanEditScope.ts +8 -3
- package/codeAgent/tools/AkanToolPack.ts +4 -10
- package/codeAgent/tools/AkanVerifier.ts +13 -31
- package/codeAgent/tools/akanCodePlugins.test.ts +60 -58
- package/commandDecorators/workspaceVersionCheck.test.ts +17 -7
- package/commandDecorators/workspaceVersionCheck.ts +4 -1
- package/commentDensityScanner.test.ts +121 -0
- package/commentDensityScanner.ts +101 -0
- package/componentSizeScanner.test.ts +36 -0
- package/componentSizeScanner.ts +52 -0
- package/componentWriteChainScanner.test.ts +137 -0
- package/componentWriteChainScanner.ts +104 -0
- package/conventionSet.test.ts +73 -0
- package/conventionSet.ts +67 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +6 -1
- package/deprecatedCommentScanner.test.ts +54 -0
- package/deprecatedCommentScanner.ts +30 -0
- package/endpointReach.test.ts +58 -0
- package/endpointReach.ts +80 -0
- package/executors.test.ts +0 -4
- package/executors.ts +18 -2
- package/index.ts +1 -0
- package/integration/devStability.integration.test.ts +9 -5
- package/kindBranchScanner.test.ts +89 -0
- package/kindBranchScanner.ts +158 -0
- package/lint/__fixtures__/no-oversized-list-limit/bad.tsx +18 -0
- package/lint/__fixtures__/no-oversized-list-limit/good.tsx +23 -0
- package/lint/no-oversized-list-limit.grit +73 -0
- package/linter.test.ts +12 -0
- package/linter.ts +35 -12
- package/maintainabilityBaseline.test.ts +280 -0
- package/maintainabilityBaseline.ts +220 -0
- package/maintainabilityContract.test.ts +846 -0
- package/maintainabilityContract.ts +404 -0
- package/maintainabilityExceptionScanner.test.ts +90 -0
- package/maintainabilityExceptionScanner.ts +85 -0
- package/maintainabilityLimits.ts +60 -0
- package/maintainabilityReport.test.ts +130 -0
- package/maintainabilityReport.ts +153 -0
- package/mobile/desktopRelease.test.ts +94 -23
- package/mobile/desktopRelease.ts +83 -45
- package/mobile/nativeApi.test.ts +4 -1
- package/mobile/nativeApi.ts +3 -3
- package/mobile/nativeApp.test.ts +2 -2
- package/mobile/nativeApp.ts +18 -11
- package/mobile/nativeConfig.test.ts +16 -8
- package/mobile/nativeConfig.ts +1 -1
- package/mobile/updateUploader.test.ts +34 -0
- package/mobile/updateUploader.ts +9 -6
- package/package.json +3 -2
- package/platformTest/LinuxDockerTarget.ts +3 -0
- package/{codeAgent/feedback → preview}/PreviewChrome.ts +13 -7
- package/preview/PreviewView.ts +219 -0
- package/preview/index.ts +2 -0
- package/preview/previewView.test.ts +76 -0
- package/prompter.ts +7 -0
- package/qualityScanner.test.ts +75 -0
- package/qualityScanner.ts +17 -1
- package/routeSourceValidator.test.ts +11 -2
- package/routeSourceValidator.ts +14 -2
- package/serviceLoopCallScanner.test.ts +56 -0
- package/serviceLoopCallScanner.ts +122 -0
- package/signalMutationIndex.test.ts +15 -0
- package/signalMutationIndex.ts +1 -1
- package/sourceComments.test.ts +45 -0
- package/sourceComments.ts +70 -0
- package/storeAccessScanner.test.ts +84 -0
- package/storeAccessScanner.ts +71 -0
- package/storeSource.ts +159 -0
- package/storeWriteScanner.test.ts +292 -0
- package/storeWriteScanner.ts +50 -0
- package/testOnlyExportScanner.test.ts +83 -0
- package/testOnlyExportScanner.ts +124 -0
- package/typeChecker.ts +32 -0
- package/typecheck/projectTypecheck.test.ts +18 -0
- package/typecheck/projectTypecheck.ts +29 -5
- package/typecheck/typecheck.proc.ts +6 -0
- package/typecheck/typecheckFailure.ts +13 -0
- package/types.ts +1 -0
- package/workflow/artifacts.ts +16 -2
- package/workflow/executor.ts +22 -7
- package/workflow/guardCatalog.test.ts +59 -0
- package/workflow/guardCatalog.ts +108 -0
- package/workflow/index.ts +4 -0
- package/workflow/interactionScaffold.ts +122 -0
- package/workflow/mutationScaffold.ts +519 -0
- package/workflow/mutationSource.test.ts +108 -0
- package/workflow/mutationSource.ts +219 -0
- package/workflow/plan.ts +54 -0
- package/workflow/render.ts +19 -4
- package/workflow/source.test.ts +185 -0
- package/workflow/source.ts +151 -24
- package/workflow/types.ts +18 -1
- package/workspaceLayout.ts +2 -0
- package/writePath.test.ts +109 -0
- package/writePath.ts +219 -0
- package/writePlacementScanner.test.ts +77 -0
- package/writePlacementScanner.ts +50 -0
- package/writingActionIndex.test.ts +99 -0
- package/writingActionIndex.ts +63 -0
- package/codeAgent/feedback/PreviewView.ts +0 -95
- /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
|
|
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
|
|
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
|
-
|
|
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
|
|
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": [
|
|
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": [
|
package/cloud/cloudUpdates.ts
CHANGED
|
@@ -102,8 +102,9 @@ export class CloudUpdates {
|
|
|
102
102
|
const problems = (record.problems ?? []).map((problem) => ` - ${problem}`).join("\n");
|
|
103
103
|
if (record.status === "rejected") throw new ExitCodeError(`The project rejected ${label}:\n${problems}`, 2);
|
|
104
104
|
const manifestUrl =
|
|
105
|
-
typeof record.manifestUrl === "string" ? record.manifestUrl : `${url}/${platform}-${arch}/${channel}.json`;
|
|
106
|
-
const
|
|
105
|
+
typeof record.manifestUrl === "string" ? record.manifestUrl : `${url}/${platform}-${arch}/${channel}/app.json`;
|
|
106
|
+
const files = 1 + release.manifestFiles.length + release.payloads.length;
|
|
107
|
+
const what = `${label} (${release.manifest.bundle}, ${files} files)`;
|
|
107
108
|
Logger.rawLog(
|
|
108
109
|
record.status === "published"
|
|
109
110
|
? `Published ${what}. Installed apps read ${manifestUrl}.`
|
|
@@ -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
|
|
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
|
-
|
|
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[]) {
|
package/codeAgent/index.ts
CHANGED
|
@@ -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: "
|
|
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
|
|
21
|
-
|
|
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
|
|
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 `
|
|
42
|
-
|
|
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
|
-
`
|
|
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.
|