@akanjs/devkit 3.0.0-beta.19 → 3.0.0-beta.20
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/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +1 -4
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +1 -3
- package/akanApp/akanApp.host.ts +64 -250
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +30 -39
- package/akanConfig/akanConfig.ts +40 -136
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.ts +25 -59
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +3 -19
- package/artifact/implicitRootLayout.ts +34 -91
- package/artifact/routeSeedIndex.ts +11 -44
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +4 -14
- package/capacitorApp.ts +86 -235
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +33 -57
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +3 -2
- package/cloud/globalConfig.ts +64 -31
- package/codeAgent/agent/AkanCodeServices.ts +15 -57
- package/codeAgent/agent/AkanContextFiles.ts +5 -28
- package/codeAgent/agent/CodeAgent.ts +33 -108
- package/codeAgent/agent/CodeAgentAsks.ts +7 -30
- package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
- package/codeAgent/agent/CodeAgentGate.ts +8 -34
- package/codeAgent/agent/CodeAgentProxy.ts +4 -13
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
- package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
- package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
- package/codeAgent/agent/CodeAgentUi.ts +2 -7
- package/codeAgent/agent/CodeMailbox.ts +7 -34
- package/codeAgent/agent/CodeSessionFork.ts +4 -19
- package/codeAgent/agent/CodeSessionIndex.ts +5 -31
- package/codeAgent/agent/SubagentPool.ts +48 -58
- package/codeAgent/agent/akanCodeModel.ts +5 -39
- package/codeAgent/agent/akanCodePaths.ts +4 -24
- package/codeAgent/agent/akanContextFiles.test.ts +0 -2
- package/codeAgent/agent/akanSystemPrompt.ts +1 -9
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +5 -28
- package/codeAgent/agent/codeMailbox.test.ts +0 -4
- package/codeAgent/agent/codeSessionFork.test.ts +0 -2
- package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
- package/codeAgent/agent/consoleToStderr.ts +2 -12
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +3 -18
- package/codeAgent/feedback/PreviewChrome.ts +5 -29
- package/codeAgent/feedback/PreviewFeedback.ts +2 -9
- package/codeAgent/feedback/PreviewView.ts +3 -15
- package/codeAgent/feedback/TurnFeedback.ts +2 -17
- package/codeAgent/feedback/VerifyFeedback.ts +1 -7
- package/codeAgent/feedback/previewChrome.test.ts +1 -1
- package/codeAgent/index.ts +2 -10
- package/codeAgent/skills/akan-code/SKILL.md +12 -4
- package/codeAgent/tools/AkanCli.ts +4 -16
- package/codeAgent/tools/AkanCodePlugins.ts +21 -43
- package/codeAgent/tools/AkanEditScope.ts +2 -12
- package/codeAgent/tools/AkanEnvKeys.ts +3 -11
- package/codeAgent/tools/AkanToolPack.ts +6 -28
- package/codeAgent/tools/AkanVerifier.ts +1 -8
- package/codeAgent/tools/AskToolPack.ts +3 -15
- package/codeAgent/tools/MailToolPack.ts +0 -9
- package/codeAgent/tools/McpClient.ts +3 -24
- package/codeAgent/tools/McpOAuth.ts +6 -41
- package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
- package/codeAgent/tools/McpServerConfig.ts +7 -26
- package/codeAgent/tools/McpSignIn.ts +46 -49
- package/codeAgent/tools/McpTokenStore.ts +6 -17
- package/codeAgent/tools/McpToolPack.ts +107 -35
- package/codeAgent/tools/SessionToolPack.ts +1 -11
- package/codeAgent/tools/WebToolPack.ts +1 -7
- package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
- package/codeAgent/tools/askToolPack.test.ts +0 -3
- package/codeAgent/tools/mcpOAuth.test.ts +315 -27
- package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +44 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +1 -9
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +66 -144
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +27 -78
- package/devkitUtils.test.ts +47 -19
- package/executors.test.ts +114 -60
- package/executors.ts +165 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +1 -1
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +14 -38
- package/frontendBuild/autoImportSync.ts +15 -67
- package/frontendBuild/buildRouteClient.test.ts +10 -36
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- package/frontendBuild/cssImportResolver.ts +5 -18
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +5 -13
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +27 -52
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +7 -31
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +7 -47
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/devWatchBatch.test.ts +14 -18
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
- package/index.ts +3 -15
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +9 -29
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/gritRules.test.ts +6 -23
- package/linter.test.ts +1 -2
- package/linter.ts +22 -115
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +2 -2
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +3 -14
- package/platformTest/IdleWatchedProcess.test.ts +5 -13
- package/platformTest/IdleWatchedProcess.ts +1 -5
- package/platformTest/LinuxDockerTarget.ts +20 -35
- package/platformTest/PlatformTestRun.test.ts +5 -9
- package/platformTest/PlatformTestTarget.ts +24 -2
- package/platformTest/TestSnapshot.test.ts +6 -14
- package/platformTest/TestSnapshot.ts +1 -5
- package/platformTest/WindowsSshTarget.ts +6 -22
- package/prompter.ts +3 -6
- package/qualityScanner.test.ts +6 -24
- package/qualityScanner.ts +196 -284
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.ts +7 -30
- package/scanInfo.ts +82 -214
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/subspace.test.ts +6 -25
- package/subspace.ts +31 -109
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +49 -34
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +85 -146
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/source.ts +33 -56
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -33,15 +33,12 @@ describe("CodeMailbox", () => {
|
|
|
33
33
|
one.send("bbbb2222", "the adapter moved to srvkit");
|
|
34
34
|
await Bun.sleep(CodeMailbox.pollMs + 400);
|
|
35
35
|
expect(got).toEqual(["the adapter moved to srvkit"]);
|
|
36
|
-
// The poll runs again; a message already handed over must not be handed over twice.
|
|
37
36
|
await Bun.sleep(CodeMailbox.pollMs + 200);
|
|
38
37
|
expect(got).toEqual(["the adapter moved to srvkit"]);
|
|
39
38
|
});
|
|
40
39
|
|
|
41
|
-
/** What was already in the inbox belongs to an earlier run, which read it or never will. */
|
|
42
40
|
test("a session opening on a full inbox does not replay it", async () => {
|
|
43
41
|
const dir = mkdtempSync(path.join(tmpdir(), "akan-mail-"));
|
|
44
|
-
// Opened once to create the directories, then closed, then seeded the way a previous run would have.
|
|
45
42
|
open(dir, "cccc3333", "seeded").close();
|
|
46
43
|
const inbox = path.join(dir, "inbox", "cccc3333.jsonl");
|
|
47
44
|
writeFileSync(inbox, `${JSON.stringify({ from: "x", fromName: "x", text: "old" })}\n`);
|
|
@@ -67,7 +64,6 @@ describe("CodeMailbox", () => {
|
|
|
67
64
|
open(dir, "bbbb3333", "store-other");
|
|
68
65
|
expect(one.find("store-work")?.id).toBe("bbbb2222");
|
|
69
66
|
expect(one.find("bbbb2")?.id).toBe("bbbb2222");
|
|
70
|
-
// Two names starting with `store` — sending to the wrong session is worse than not sending.
|
|
71
67
|
expect(one.find("store")).toBeUndefined();
|
|
72
68
|
});
|
|
73
69
|
|
|
@@ -34,7 +34,6 @@ describe("CodeSessionFork", () => {
|
|
|
34
34
|
const copied = records(forked.file);
|
|
35
35
|
expect(copied.find((record) => record.type === "session")?.id).toBe(forked.id);
|
|
36
36
|
expect(copied.filter((record) => record.type === "message").length).toBe(1);
|
|
37
|
-
// Two live agents sharing one id would share one inbox, one presence entry and one resume target.
|
|
38
37
|
expect(CodeSessionIndex.fileOf(dir, forked.id)).toBe(forked.file);
|
|
39
38
|
expect(CodeSessionIndex.fileOf(dir, id)).toBe(file);
|
|
40
39
|
});
|
|
@@ -49,7 +48,6 @@ describe("CodeSessionFork", () => {
|
|
|
49
48
|
test("the copy says it is one, and takes a name when given one", () => {
|
|
50
49
|
const { dir, id } = seed();
|
|
51
50
|
expect(records(CodeSessionFork.fork(dir, id).file).at(-1)?.name).toBe("read-the-guard-fork");
|
|
52
|
-
// A name somebody typed is kept as typed, the way `/name` keeps one; only a derived name is kebabed.
|
|
53
51
|
expect(records(CodeSessionFork.fork(dir, id, "try the other guard").file).at(-1)?.name).toBe("try the other guard");
|
|
54
52
|
});
|
|
55
53
|
|
|
@@ -27,7 +27,6 @@ describe("CodeSessionIndex", () => {
|
|
|
27
27
|
assistant("done"),
|
|
28
28
|
user("now wire it up"),
|
|
29
29
|
{ type: "session_info", name: "add-a-comment-module" },
|
|
30
|
-
// A rename appends rather than editing, so the last entry has to win.
|
|
31
30
|
{ type: "session_info", name: "comment-module" },
|
|
32
31
|
]);
|
|
33
32
|
const [entry] = CodeSessionIndex.list(dir);
|
|
@@ -45,11 +44,19 @@ describe("CodeSessionIndex", () => {
|
|
|
45
44
|
expect(CodeSessionIndex.list(dir).map((entry) => entry.id)).toEqual(["s3"]);
|
|
46
45
|
});
|
|
47
46
|
|
|
47
|
+
test("a session past the read limit is still listed, from its opening lines", () => {
|
|
48
|
+
const dir = mkdtempSync(path.join(tmpdir(), "akan-sessions-"));
|
|
49
|
+
const filler = assistant("x".repeat(64 * 1024));
|
|
50
|
+
write(dir, "big", [session("big"), user("port the dashboard"), ...Array.from({ length: 10 }, () => filler)]);
|
|
51
|
+
const [entry] = CodeSessionIndex.list(dir);
|
|
52
|
+
expect(entry?.id).toBe("big");
|
|
53
|
+
expect(entry?.opening).toBe("port the dashboard");
|
|
54
|
+
});
|
|
55
|
+
|
|
48
56
|
test("an id resolves to its file, and an unknown one to nothing", () => {
|
|
49
57
|
const dir = mkdtempSync(path.join(tmpdir(), "akan-sessions-"));
|
|
50
58
|
const file = write(dir, "s4", [session("s4"), user("hi")]);
|
|
51
59
|
expect(CodeSessionIndex.fileOf(dir, "s4")).toBe(file);
|
|
52
|
-
// Resuming a typo has to fail rather than open a new session that looks like one that lost its history.
|
|
53
60
|
expect(CodeSessionIndex.fileOf(dir, "s5")).toBeUndefined();
|
|
54
61
|
});
|
|
55
62
|
|
|
@@ -61,12 +68,10 @@ describe("CodeSessionIndex", () => {
|
|
|
61
68
|
describe("codeAgentSessionName", () => {
|
|
62
69
|
test("names a session after what it was asked, in one glanceable line", () => {
|
|
63
70
|
expect(codeAgentSessionName("Add a comment module to apps/minimal")).toBe("add-a-comment-module-to-apps-minimal");
|
|
64
|
-
// Cut on a word, never mid-word: half a word reads as a typo rather than a shortening.
|
|
65
71
|
expect(codeAgentSessionName("Add a comment module to apps/minimal and wire up its dictionary")).toBe(
|
|
66
72
|
"add-a-comment-module-to-apps-minimal-and",
|
|
67
73
|
);
|
|
68
74
|
expect(codeAgentSessionName("안녕, 모듈 하나 만들어줘")).toBe("안녕-모듈-하나-만들어줘");
|
|
69
|
-
// A prompt that is all code says nothing about itself; a name is still better than none.
|
|
70
75
|
expect(codeAgentSessionName("```ts\nconst a = 1;\n```")).toBe("session");
|
|
71
76
|
});
|
|
72
77
|
});
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import { Console } from "node:console";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
* The engine already installs a guard that rewrites `process.stdout.write`, which is enough on Node because
|
|
7
|
-
* `console.log` routes through it there. Bun's global `console` is native and writes to fd 1 directly, so it
|
|
8
|
-
* sails straight past that guard and corrupts the JSONL stream — measured: 203 non-JSON lines in one turn that
|
|
9
|
-
* logged while a tool ran.
|
|
10
|
-
*
|
|
11
|
-
* **This is a side-effect module and must be the first import of any RPC entry.** An assignment written beside
|
|
12
|
-
* the static imports would run *after* them: ESM hoists imports and evaluates those modules first, so anything
|
|
13
|
-
* the engine logs at module-evaluation time would already have escaped.
|
|
14
|
-
*/
|
|
3
|
+
// Bun's native `console` writes to fd 1 directly, past the engine's stdout guard, corrupting the RPC stream.
|
|
4
|
+
// Must be the first import of an RPC entry: ESM evaluates imports first, so an inline assignment runs too late.
|
|
15
5
|
globalThis.console = new Console({
|
|
16
6
|
stdout: process.stderr,
|
|
17
7
|
stderr: process.stderr,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { closeSync, openSync, readFileSync, readSync, statSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const readHead = (file: string, maxBytes: number) => {
|
|
4
|
+
const fd = openSync(file, "r");
|
|
5
|
+
try {
|
|
6
|
+
const head = Buffer.alloc(maxBytes);
|
|
7
|
+
return head.subarray(0, readSync(fd, head, 0, maxBytes, 0)).toString("utf8");
|
|
8
|
+
} finally {
|
|
9
|
+
closeSync(fd);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const readJsonLines = (file: string, maxBytes?: number) => {
|
|
14
|
+
try {
|
|
15
|
+
const text =
|
|
16
|
+
maxBytes !== undefined && statSync(file).size > maxBytes ? readHead(file, maxBytes) : readFileSync(file, "utf8");
|
|
17
|
+
return text.split("\n").filter((line) => !!line.trim());
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const parseJsonLine = <T>(line: string) => {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(line) as T;
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -15,14 +15,7 @@ export interface DevLogFeedbackOptions {
|
|
|
15
15
|
const errorPattern = /\bERROR\b|\bFATAL\b|\[stderr\]|error:|Error:|✖|Unhandled|Cannot find|is not a function/;
|
|
16
16
|
const noisePattern = /DEBUG|\bWARN\b|MCP catalogue|rate limit/;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
* Feeds back what the dev server said after a turn edited code.
|
|
20
|
-
*
|
|
21
|
-
* `akan start` writes `local/apps/<app>/runtime/dev.log` with no ANSI, nothing truncated, and every process of
|
|
22
|
-
* the app in arrival order including the dev host's own build output — which reaches no other file. That is the
|
|
23
|
-
* one thing a coding agent working on an akan app has and a general one does not: lint and typecheck can both
|
|
24
|
-
* pass on code that dies the moment it runs.
|
|
25
|
-
*/
|
|
18
|
+
// dev.log is the only file carrying the dev host's own build output; lint and typecheck pass on code that dies at run.
|
|
26
19
|
export class DevLogFeedback implements TurnFeedbackSource {
|
|
27
20
|
readonly key = "dev-server";
|
|
28
21
|
readonly #options: Required<DevLogFeedbackOptions>;
|
|
@@ -41,9 +34,7 @@ export class DevLogFeedback implements TurnFeedbackSource {
|
|
|
41
34
|
async observe() {
|
|
42
35
|
const running = this.#options.apps.filter((app) => existsSync(this.#pathOf(app)));
|
|
43
36
|
if (!running.length) return undefined;
|
|
44
|
-
// A turn that wrote nothing cannot have broken the
|
|
45
|
-
// somebody else is driving reopens a turn that only read a file — and a reopened turn looks, from the
|
|
46
|
-
// transcript, exactly like an agent that will not stop.
|
|
37
|
+
// A turn that wrote nothing cannot have broken the app; without this gate another person's dev server reopens it.
|
|
47
38
|
if (!(await AkanEditScope.since(this.#options.cwd, this.#baseline)).paths.length) return undefined;
|
|
48
39
|
await Bun.sleep(this.#options.graceMs);
|
|
49
40
|
const reports: string[] = [];
|
|
@@ -77,12 +68,7 @@ export class DevLogFeedback implements TurnFeedbackSource {
|
|
|
77
68
|
return path.join(this.#options.workspaceRoot, "local", "apps", app, "runtime", "dev.log");
|
|
78
69
|
}
|
|
79
70
|
|
|
80
|
-
/**
|
|
81
|
-
* The dev server's own URL, read back from the line `akan start` prints when the app comes up.
|
|
82
|
-
*
|
|
83
|
-
* The port is allocated, not predicted — a second checkout or a stale listener shifts it — so the log is the
|
|
84
|
-
* only place that knows which one this session actually got.
|
|
85
|
-
*/
|
|
71
|
+
/** The port is allocated, not predicted, so the log's `ready` line is the only record of this session's URL. */
|
|
86
72
|
static async previewUrl(workspaceRoot: string, app: string) {
|
|
87
73
|
const file = path.join(workspaceRoot, "local", "apps", app, "runtime", "dev.log");
|
|
88
74
|
if (!existsSync(file)) return undefined;
|
|
@@ -90,7 +76,6 @@ export class DevLogFeedback implements TurnFeedbackSource {
|
|
|
90
76
|
return matches.at(-1)?.[1];
|
|
91
77
|
}
|
|
92
78
|
|
|
93
|
-
/** A dev server that is not running has no log, which is not an error — the source simply reports nothing. */
|
|
94
79
|
static #sizeOf(file: string) {
|
|
95
80
|
return existsSync(file) ? statSync(file).size : 0;
|
|
96
81
|
}
|
|
@@ -2,13 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
* Which `Bun.WebView` backend the preview probe runs on, and what an image needs to carry one.
|
|
7
|
-
*
|
|
8
|
-
* `webkit` is the macOS system framework — zero install, and it throws on every other platform — so anywhere
|
|
9
|
-
* else the probe drives Chrome. The generated akan image installs `ca-certificates` and `tzdata` and nothing
|
|
10
|
-
* else, which is why the install steps are named here instead of being reinvented per image.
|
|
11
|
-
*/
|
|
5
|
+
// Bun.WebView's `webkit` backend is the macOS system framework and throws on every other platform.
|
|
12
6
|
export class PreviewChrome {
|
|
13
7
|
/** Read by `Bun.WebView` itself; listed so the availability check agrees with what the launcher will do. */
|
|
14
8
|
static readonly pathEnvKey = "BUN_CHROME_PATH";
|
|
@@ -27,12 +21,7 @@ export class PreviewChrome {
|
|
|
27
21
|
"chrome",
|
|
28
22
|
];
|
|
29
23
|
|
|
30
|
-
/**
|
|
31
|
-
* macOS keeps browsers in app bundles, off PATH, and Bun looks inside these before reporting none.
|
|
32
|
-
*
|
|
33
|
-
* The list has to match Bun's or the two disagree in the expensive direction: refusing to probe on a machine
|
|
34
|
-
* whose browser the launcher would have found.
|
|
35
|
-
*/
|
|
24
|
+
/** Must match the macOS app bundles Bun searches, or the probe refuses a browser the launcher would find. */
|
|
36
25
|
static readonly appBundles = [
|
|
37
26
|
"Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
38
27
|
"Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
@@ -67,29 +56,16 @@ export class PreviewChrome {
|
|
|
67
56
|
return PreviewChrome.backend === "webkit" ? process.platform === "darwin" : !!PreviewChrome.executable();
|
|
68
57
|
}
|
|
69
58
|
|
|
70
|
-
/**
|
|
71
|
-
* The `backend` value to construct a view with.
|
|
72
|
-
*
|
|
73
|
-
* Two flags are added on the Chrome path, and Bun adds neither:
|
|
74
|
-
* - Chromium refuses to run as uid 0 unless its sandbox is off, and uid 0 is the ordinary uid in a container.
|
|
75
|
-
* - Docker gives a container 64MB of `/dev/shm` by default, below what Chromium's renderer allocates, and the
|
|
76
|
-
* crash it produces looks like a page that never loads rather than a resource limit.
|
|
77
|
-
*/
|
|
78
59
|
static backendOption() {
|
|
79
60
|
if (PreviewChrome.backend === "webkit") return "webkit";
|
|
61
|
+
// Docker's default 64MB /dev/shm is below what Chromium's renderer allocates; the crash looks like a hung load.
|
|
80
62
|
const argv = ["--disable-dev-shm-usage"];
|
|
63
|
+
// Chromium refuses uid 0 with its sandbox on, and uid 0 is the ordinary uid in a container.
|
|
81
64
|
if (process.getuid?.() === 0) argv.push("--no-sandbox");
|
|
82
65
|
return { type: "chrome" as const, argv };
|
|
83
66
|
}
|
|
84
67
|
|
|
85
|
-
/**
|
|
86
|
-
* `RUN` bodies for an image whose agent has to look at the page it just changed — an akan app's
|
|
87
|
-
* `docker.preRuns`, or a coding-agent pod.
|
|
88
|
-
*
|
|
89
|
-
* The font packages are not decoration: a container with no fonts renders every glyph as a box, so a
|
|
90
|
-
* screenshot says nothing and the "text is the same colour as its background" probe reads a blank page.
|
|
91
|
-
* `fonts-noto-cjk` is what keeps a Korean UI legible.
|
|
92
|
-
*/
|
|
68
|
+
/** The generated image has no browser or fonts; a fontless container renders every glyph as a box. */
|
|
93
69
|
static readonly dockerRuns = [
|
|
94
70
|
"apt-get update && apt-get install -y --no-install-recommends chromium fonts-liberation fonts-noto-color-emoji fonts-noto-cjk && rm -rf /var/lib/apt/lists/*",
|
|
95
71
|
];
|
|
@@ -8,18 +8,11 @@ export interface PreviewFeedbackOptions {
|
|
|
8
8
|
previewUrl?: string;
|
|
9
9
|
/** Every akan route sits under `/:lang`, so a bare path renders nothing. */
|
|
10
10
|
routes?: string[];
|
|
11
|
-
/** Only a model whose input includes images gets
|
|
11
|
+
/** Only a model whose input includes images gets a screenshot attached. */
|
|
12
12
|
canSeeImages?: boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
* Looks at the page after a turn changed the UI.
|
|
17
|
-
*
|
|
18
|
-
* akan is a UI framework, so "does it run" is half the question. The default findings are **text** — page
|
|
19
|
-
* console errors, an empty body, an error overlay, horizontal overflow, a dropped colour slot — because they
|
|
20
|
-
* cost a few hundred tokens, work with any model, and name the defect rather than depicting it. A screenshot is
|
|
21
|
-
* attached only when the active model can actually read one.
|
|
22
|
-
*/
|
|
15
|
+
// Findings are text, not screenshots: a few hundred tokens, readable by any model, and naming the defect.
|
|
23
16
|
export class PreviewFeedback implements TurnFeedbackSource {
|
|
24
17
|
readonly key = "preview";
|
|
25
18
|
readonly #options: PreviewFeedbackOptions;
|
|
@@ -18,22 +18,13 @@ interface WebViewLike {
|
|
|
18
18
|
readonly title: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
* `Bun.WebView` rather than a downloaded browser: on macOS the `webkit` backend is the system framework, so
|
|
25
|
-
* there is no Chromium to fetch and no executable path to track, and everywhere else it drives whatever Chrome
|
|
26
|
-
* the image carries. It is marked experimental, which is why the whole surface is behind this adapter — a Bun
|
|
27
|
-
* minor that changes it should break one file.
|
|
28
|
-
*
|
|
29
|
-
* Which backend, and what an image needs for it, is {@link PreviewChrome}.
|
|
30
|
-
*/
|
|
21
|
+
// Bun.WebView rather than a downloaded browser: on macOS its webkit backend needs no Chromium at all.
|
|
22
|
+
// It is experimental, so its whole surface stays behind this adapter; a Bun minor that changes it breaks one file.
|
|
31
23
|
export class PreviewView {
|
|
32
24
|
static get available() {
|
|
33
25
|
return typeof (Bun as unknown as { WebView?: unknown }).WebView === "function" && PreviewChrome.available;
|
|
34
26
|
}
|
|
35
27
|
|
|
36
|
-
/** Why `available` is false, for a host that should say so rather than skip the check in silence. */
|
|
37
28
|
static unavailableReason() {
|
|
38
29
|
if (typeof (Bun as unknown as { WebView?: unknown }).WebView !== "function")
|
|
39
30
|
return "This Bun build has no WebView, so the page preview is off.";
|
|
@@ -76,10 +67,7 @@ export class PreviewView {
|
|
|
76
67
|
}
|
|
77
68
|
}
|
|
78
69
|
|
|
79
|
-
|
|
80
|
-
* `evaluate` takes an expression, not a statement list — `const x = 1; x` is a `SyntaxError` — so each probe is
|
|
81
|
-
* wrapped in an IIFE. Only one may be in flight per view.
|
|
82
|
-
*/
|
|
70
|
+
// evaluate() takes one expression (a statement list is a SyntaxError), hence the IIFEs; one call in flight per view.
|
|
83
71
|
const pageErrorsExpression = `(() => {
|
|
84
72
|
const found = [];
|
|
85
73
|
const root = document.getElementById("__next") || document.body;
|
|
@@ -26,20 +26,8 @@ export interface TurnFeedbackOptions {
|
|
|
26
26
|
onNotice?: (message: string) => void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
* The agent cannot see that the dev server died, that a generated barrel went stale, or that the page it built
|
|
33
|
-
* renders blank — so it finishes, satisfied, on a broken tree. Each source watermarks at the start of a run and
|
|
34
|
-
* reports at the end; a report is delivered as a follow-up message, which opens a new turn.
|
|
35
|
-
*
|
|
36
|
-
* **It is a follow-up, not a veto.** `agent_end` has no result type — of the engine's hooks only
|
|
37
|
-
* `before_agent_start`, `message_end`, `tool_call`, `tool_result`, `input`, `user_bash`, the `session_before_*`
|
|
38
|
-
* pair, `context` and `resources_discover` can change an outcome — so there is no way to refuse to end a turn.
|
|
39
|
-
*
|
|
40
|
-
* **The budget goes in before the feature.** A source that reports the same unfixable error every turn would
|
|
41
|
-
* otherwise loop until the context window or the wallet runs out, and the loop looks like progress from outside.
|
|
42
|
-
*/
|
|
29
|
+
// The engine's `agent_end` hook cannot veto the end of a turn, so a finding is a follow-up that opens a new one.
|
|
30
|
+
// The budget is load-bearing: without it one unfixable error reopens turns until the context or the wallet runs out.
|
|
43
31
|
export class TurnFeedback {
|
|
44
32
|
readonly #sources: TurnFeedbackSource[];
|
|
45
33
|
readonly #options: TurnFeedbackOptions;
|
|
@@ -65,7 +53,6 @@ export class TurnFeedback {
|
|
|
65
53
|
};
|
|
66
54
|
}
|
|
67
55
|
|
|
68
|
-
/** Exposed so a host can drive the same machine without an engine session behind it. */
|
|
69
56
|
async collect() {
|
|
70
57
|
const texts: string[] = [];
|
|
71
58
|
const images: TurnFeedbackImage[] = [];
|
|
@@ -90,8 +77,6 @@ export class TurnFeedback {
|
|
|
90
77
|
}
|
|
91
78
|
}
|
|
92
79
|
if (!texts.length) return undefined;
|
|
93
|
-
// Said out loud, because the turn that follows is one nobody asked for: the transcript shows more tool
|
|
94
|
-
// calls after the answer and carries nothing that names who sent the agent back to work.
|
|
95
80
|
this.#options.onNotice?.(`${reopened.join(" · ")} reopened the turn — esc stops it`);
|
|
96
81
|
const text = [
|
|
97
82
|
"The previous turn left problems you cannot see from the transcript. Fix them, then stop.",
|
|
@@ -8,13 +8,7 @@ export interface VerifyFeedbackOptions {
|
|
|
8
8
|
readOnly?: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
* Runs the validation chain over whatever a turn changed and feeds the failures back.
|
|
13
|
-
*
|
|
14
|
-
* The model has a verification tool and the observed failure is that it finishes without calling it — so this
|
|
15
|
-
* is not a second tool but a hook. Detection reads git rather than tool events, because an edit made through
|
|
16
|
-
* `bash` produces no write tool call and is exactly the kind a model forgets to verify.
|
|
17
|
-
*/
|
|
11
|
+
// A hook, not a reliance on the akan_verify tool: the observed failure is a model finishing without calling it.
|
|
18
12
|
export class VerifyFeedback implements TurnFeedbackSource {
|
|
19
13
|
readonly key = "verify";
|
|
20
14
|
readonly #options: VerifyFeedbackOptions;
|
|
@@ -67,7 +67,7 @@ describe("PreviewChrome", () => {
|
|
|
67
67
|
const step = PreviewChrome.dockerRuns.join(" ");
|
|
68
68
|
expect(step).toContain("chromium");
|
|
69
69
|
expect(step).toContain("fonts-noto-cjk");
|
|
70
|
-
//
|
|
70
|
+
// Leftover apt lists bloat every image layer that takes this step.
|
|
71
71
|
expect(step).toContain("rm -rf /var/lib/apt/lists/*");
|
|
72
72
|
});
|
|
73
73
|
});
|
package/codeAgent/index.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Importing this barrel loads the engine, which costs ~122MiB resident. A host that only needs the cheap
|
|
5
|
-
* pieces — the model reference, the stream printer, the sub-agent kinds — reaches for the owning module
|
|
6
|
-
* instead (`@akanjs/devkit/codeAgent/agent/akanCodeModel`), the same way `@akanjs/devkit` itself asks to be
|
|
7
|
-
* used. `agent/consoleToStderr` is deliberately absent: it is a side-effect module that takes over
|
|
8
|
-
* `globalThis.console`, and it has to be the first import of an RPC entry rather than a re-export of this one.
|
|
9
|
-
*/
|
|
10
|
-
|
|
1
|
+
// Importing this barrel loads the engine (~122MiB resident); a host needing cheap pieces imports the owning module.
|
|
2
|
+
// agent/consoleToStderr is deliberately absent: it takes over globalThis.console, so an RPC entry imports it first.
|
|
11
3
|
export * from "./agent/AkanCodeServices";
|
|
12
4
|
export * from "./agent/akanCodeModel";
|
|
13
5
|
export * from "./agent/akanCodePaths";
|
|
@@ -15,8 +15,9 @@ which command makes it take effect.
|
|
|
15
15
|
|
|
16
16
|
## MCP servers
|
|
17
17
|
|
|
18
|
-
Declared in
|
|
19
|
-
|
|
18
|
+
Declared in **`~/.akan/code/mcp.json`**, which every repo reads, or in this repo's **`.akan/code/mcp.json`**,
|
|
19
|
+
which wins a name both declare — in the same `mcpServers` shape Claude, Cursor and VS Code already write, so a
|
|
20
|
+
block can be pasted across rather than translated. VS Code's `servers` spelling is read too, and
|
|
20
21
|
whichever key the file already uses is the one a write goes back into.
|
|
21
22
|
|
|
22
23
|
```jsonc
|
|
@@ -41,7 +42,9 @@ From the prompt:
|
|
|
41
42
|
/mcp <name> the tools that one server published
|
|
42
43
|
/mcp add <name> <command> [args…]
|
|
43
44
|
/mcp add <name> <https://…>
|
|
44
|
-
/mcp
|
|
45
|
+
/mcp add --local <name> … this repo's file only, not ~/.akan/code/mcp.json
|
|
46
|
+
/mcp remove <name> this repo's file first, else ~/.akan/code/mcp.json
|
|
47
|
+
/mcp remove --local <name>
|
|
45
48
|
/mcp login <name> OAuth, in the browser
|
|
46
49
|
/mcp logout <name> forget the token, leaving the server declared
|
|
47
50
|
/mcp reload reopen this session so the file takes effect
|
|
@@ -52,11 +55,16 @@ session is created, so `add`, `remove`, `login` and `logout` all reload by thems
|
|
|
52
55
|
said nothing yet has nothing on disk to reopen, and says so. If you edit `mcp.json` yourself, tell the user to
|
|
53
56
|
run `/mcp reload`.
|
|
54
57
|
|
|
58
|
+
`/mcp remove` on a name both files declare takes this repo's entry and leaves the home file's in force; its
|
|
59
|
+
notice says so, and running it again removes that one too.
|
|
60
|
+
|
|
55
61
|
Sign-in is discovered, not configured: the server's `401` names its resource metadata, that names the
|
|
56
62
|
authorization server, and most hosted servers register a client on demand — so a url is usually the whole
|
|
57
63
|
entry. For a provider that issues client ids by hand, add `"oauth": { "clientId": "…", "scope": "…" }`.
|
|
58
64
|
Tokens are stored per server in `~/.akan/code/mcpAuth.json` at mode 0600, never in the repo, and refreshed
|
|
59
|
-
silently
|
|
65
|
+
silently — at connect when one has expired, and mid-session when a server refuses one, after which the call is
|
|
66
|
+
retried once. When a refresh cannot fix it, `/mcp` shows `sign-in needed` and that server's tools answer with
|
|
67
|
+
`/mcp login <name>` until the user signs in; the sign-in reloads the session, which brings the server back.
|
|
60
68
|
|
|
61
69
|
## Sessions
|
|
62
70
|
|
|
@@ -9,13 +9,7 @@ export interface AkanCliResult {
|
|
|
9
9
|
output: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
* Runs `akan` as a child process and captures what it printed.
|
|
14
|
-
*
|
|
15
|
-
* The runners behind these commands write straight to an inherited stdio and return nothing, so there is no
|
|
16
|
-
* in-process call that yields text to hand back to a model. A subprocess also isolates a crash in a build or a
|
|
17
|
-
* typechecker from the agent's own turn.
|
|
18
|
-
*/
|
|
12
|
+
// A subprocess: the akan runners print to inherited stdio and return nothing; it also isolates their crashes.
|
|
19
13
|
export class AkanCli {
|
|
20
14
|
readonly #cwd: string;
|
|
21
15
|
readonly #entry: string;
|
|
@@ -39,19 +33,13 @@ export class AkanCli {
|
|
|
39
33
|
proc.exited,
|
|
40
34
|
]);
|
|
41
35
|
clearTimeout(timer);
|
|
42
|
-
//
|
|
43
|
-
// escape sequences in a tool result are tokens the model pays for and cannot read.
|
|
36
|
+
// tsgo colours regardless of NO_COLOR, and escape sequences are tokens the model pays for and cannot read.
|
|
44
37
|
const output = stripAnsi(`${stdout}${stderr}`).trim();
|
|
45
38
|
return { command: `akan ${args.join(" ")}`, ok: exitCode === 0, exitCode, output };
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* entry is `index.js` beside this module; from source the entry belongs to the neighbouring package and is
|
|
51
|
-
* still TypeScript. `Bun.main` is the last resort and is only right when the CLI is the process — an SDK
|
|
52
|
-
* embedder's main is its own. The path is written out rather than resolved through the package name because
|
|
53
|
-
* devkit does not depend on the CLI, and must not start.
|
|
54
|
-
*/
|
|
41
|
+
// Bundled, import.meta.dir is the CLI's output dir with index.js beside it; Bun.main fits only when the CLI is the
|
|
42
|
+
// process. The path is written out because devkit does not depend on the CLI, and must not start.
|
|
55
43
|
static #resolveEntry() {
|
|
56
44
|
const candidates = [
|
|
57
45
|
path.join(import.meta.dir, "index.js"),
|
|
@@ -2,7 +2,7 @@ import type { InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { CodeAgentMcpStatus, CodeAgentProfile, CodeAgentQuestion, CodeAgentSubagent } from "akanjs/common";
|
|
3
3
|
import type { Workspace } from "../../commandDecorators";
|
|
4
4
|
import type { CodeMailbox } from "../agent/CodeMailbox";
|
|
5
|
-
import { SubagentPool } from "../agent/SubagentPool";
|
|
5
|
+
import { SubagentPool, type SubagentSpend } from "../agent/SubagentPool";
|
|
6
6
|
import { DevLogFeedback } from "../feedback/DevLogFeedback";
|
|
7
7
|
import { PreviewFeedback } from "../feedback/PreviewFeedback";
|
|
8
8
|
import { PreviewView } from "../feedback/PreviewView";
|
|
@@ -24,6 +24,8 @@ export interface AkanCodePluginsOptions {
|
|
|
24
24
|
canSeeImages: boolean;
|
|
25
25
|
/** How many `task` levels deep the owning agent is. */
|
|
26
26
|
depth: number;
|
|
27
|
+
/** The tally of the sub-agent tree the owning agent belongs to, when a `task` opened it. */
|
|
28
|
+
subagentSpend?: SubagentSpend;
|
|
27
29
|
/** Reading the current session back would spend its own window twice, so it is excluded from search. */
|
|
28
30
|
currentSessionId: () => string;
|
|
29
31
|
/** Late, because the session this announces to its peers does not exist yet when the pack is built. */
|
|
@@ -34,12 +36,6 @@ export interface AkanCodePluginsOptions {
|
|
|
34
36
|
onAsk?: (question: Omit<CodeAgentQuestion, "questionId">) => Promise<string | undefined>;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
/**
|
|
38
|
-
* Assembles the akan capability pack for one profile.
|
|
39
|
-
*
|
|
40
|
-
* Gating happens here rather than at run time: a capability the profile excludes is never constructed, so the
|
|
41
|
-
* model cannot call it and it costs nothing in the system prompt either.
|
|
42
|
-
*/
|
|
43
39
|
export interface AkanCodePluginsResult {
|
|
44
40
|
extensions: InlineExtension[];
|
|
45
41
|
/** Names the session's tool allowlist has to carry, or the model is told these tools do not exist. */
|
|
@@ -50,6 +46,7 @@ export interface AkanCodePluginsResult {
|
|
|
50
46
|
dispose: () => void;
|
|
51
47
|
}
|
|
52
48
|
|
|
49
|
+
// Gated at assembly, not at call time: a capability the profile excludes is never built and costs no prompt tokens.
|
|
53
50
|
export class AkanCodePlugins {
|
|
54
51
|
static async build(options: AkanCodePluginsOptions): Promise<AkanCodePluginsResult> {
|
|
55
52
|
const mcp = await McpToolPack.connect({
|
|
@@ -60,34 +57,28 @@ export class AkanCodePlugins {
|
|
|
60
57
|
const dispose = () => mcp?.close();
|
|
61
58
|
const mcpStatus = mcp?.status ?? [];
|
|
62
59
|
const web = new WebToolPack({ profile: options.profile });
|
|
63
|
-
const webExtension = web.extension();
|
|
64
60
|
const sessions = new SessionToolPack({
|
|
65
61
|
workspaceRoot: options.workspace.workspaceRoot,
|
|
66
62
|
cwd: options.cwd,
|
|
67
63
|
profile: options.profile,
|
|
68
64
|
currentSessionId: options.currentSessionId,
|
|
69
65
|
});
|
|
70
|
-
const sessionExtension = sessions.extension();
|
|
71
66
|
const mail = new MailToolPack({ profile: options.profile, mailbox: options.mailbox });
|
|
72
|
-
const mailExtension = mail.extension();
|
|
73
67
|
const onAsk = options.onAsk;
|
|
74
68
|
const ask = onAsk ? new AskToolPack({ profile: options.profile, ask: onAsk }) : undefined;
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
),
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
mcp: mcpStatus,
|
|
89
|
-
dispose,
|
|
90
|
-
};
|
|
69
|
+
const shared = {
|
|
70
|
+
extensions: [mcp?.extension(), web.extension(), sessions.extension(), mail.extension(), ask?.extension()].filter(
|
|
71
|
+
(entry) => !!entry,
|
|
72
|
+
),
|
|
73
|
+
toolNames: [
|
|
74
|
+
...(mcp?.toolNames ?? []),
|
|
75
|
+
...web.names(),
|
|
76
|
+
...sessions.names(),
|
|
77
|
+
...mail.names(),
|
|
78
|
+
...(ask?.names() ?? []),
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
if (!options.profile.tools.akan) return { ...shared, mcp: mcpStatus, dispose };
|
|
91
82
|
const readOnly = !options.profile.tools.builtin.includes("write");
|
|
92
83
|
const pack = new AkanToolPack({
|
|
93
84
|
workspace: options.workspace,
|
|
@@ -100,6 +91,7 @@ export class AkanCodePlugins {
|
|
|
100
91
|
cwd: options.cwd,
|
|
101
92
|
parent: options.profile,
|
|
102
93
|
depth: options.depth,
|
|
94
|
+
...(options.subagentSpend ? { spend: options.subagentSpend } : {}),
|
|
103
95
|
...(options.onNotice ? { onNotice: options.onNotice } : {}),
|
|
104
96
|
...(options.onAgents ? { onAgents: options.onAgents } : {}),
|
|
105
97
|
});
|
|
@@ -112,22 +104,9 @@ export class AkanCodePlugins {
|
|
|
112
104
|
...(options.onNotice ? { onNotice: options.onNotice } : {}),
|
|
113
105
|
}).extension(),
|
|
114
106
|
);
|
|
115
|
-
if (mcp) extensions.push(mcp.extension());
|
|
116
|
-
if (webExtension) extensions.push(webExtension);
|
|
117
|
-
if (sessionExtension) extensions.push(sessionExtension);
|
|
118
|
-
if (mailExtension) extensions.push(mailExtension);
|
|
119
|
-
if (askExtension) extensions.push(askExtension);
|
|
120
107
|
return {
|
|
121
|
-
extensions,
|
|
122
|
-
toolNames: [
|
|
123
|
-
...(await pack.names()),
|
|
124
|
-
...(subagent ? [SubagentPool.toolName] : []),
|
|
125
|
-
...(mcp?.toolNames ?? []),
|
|
126
|
-
...web.names(),
|
|
127
|
-
...sessions.names(),
|
|
128
|
-
...mail.names(),
|
|
129
|
-
...(ask?.names() ?? []),
|
|
130
|
-
],
|
|
108
|
+
extensions: [...extensions, ...shared.extensions],
|
|
109
|
+
toolNames: [...(await pack.names()), ...(subagent ? [SubagentPool.toolName] : []), ...shared.toolNames],
|
|
131
110
|
mcp: mcpStatus,
|
|
132
111
|
dispose,
|
|
133
112
|
};
|
|
@@ -140,8 +119,7 @@ export class AkanCodePlugins {
|
|
|
140
119
|
];
|
|
141
120
|
const previewUrl = await AkanCodePlugins.#previewUrl(options);
|
|
142
121
|
if (!previewUrl) return sources;
|
|
143
|
-
//
|
|
144
|
-
// alternative is a UI turn that quietly skips its only visual check and reads as a clean run.
|
|
122
|
+
// Said once here, or a UI turn silently skips its only visual check and reads as a clean run.
|
|
145
123
|
const reason = PreviewView.unavailableReason();
|
|
146
124
|
if (reason) options.onNotice?.(reason);
|
|
147
125
|
else sources.push(new PreviewFeedback({ cwd: options.cwd, previewUrl, canSeeImages: options.canSeeImages }));
|
|
@@ -9,16 +9,10 @@ export interface AkanEditScopeResult {
|
|
|
9
9
|
|
|
10
10
|
const empty: AkanEditScopeResult = { paths: [], apps: [], libs: [], needsSync: false, touchesTsx: false };
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
* What the working tree says changed, and which akan targets that implicates.
|
|
14
|
-
*
|
|
15
|
-
* Read from git rather than from tool events on purpose: a model that edits through `bash` — a heredoc, `sed`,
|
|
16
|
-
* a codemod — produces no write tool call at all, and a verification gate that trusts tool events would wave
|
|
17
|
-
* exactly those turns through.
|
|
18
|
-
*/
|
|
12
|
+
// Read from git, not tool events: an edit made through `bash` (a heredoc, sed, a codemod) produces no write tool call.
|
|
19
13
|
export class AkanEditScope {
|
|
20
14
|
static async since(cwd: string, baseline: ReadonlySet<string>): Promise<AkanEditScopeResult> {
|
|
21
|
-
const current = await AkanEditScope
|
|
15
|
+
const current = await AkanEditScope.baseline(cwd);
|
|
22
16
|
const changed = [...current].filter((line) => !baseline.has(line));
|
|
23
17
|
if (!changed.length) return empty;
|
|
24
18
|
const entries = changed.map((line) => ({ code: line.slice(0, 2), file: line.slice(3).split(" -> ").at(-1) ?? "" }));
|
|
@@ -34,10 +28,6 @@ export class AkanEditScope {
|
|
|
34
28
|
|
|
35
29
|
/** The porcelain lines before a turn, so the turn is judged on what it changed rather than on a dirty tree. */
|
|
36
30
|
static async baseline(cwd: string) {
|
|
37
|
-
return await AkanEditScope.#status(cwd);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
static async #status(cwd: string) {
|
|
41
31
|
const proc = Bun.spawn(["git", "status", "--porcelain=v1", "--untracked-files=all"], {
|
|
42
32
|
cwd,
|
|
43
33
|
stdout: "pipe",
|