@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.
Files changed (233) hide show
  1. package/abstractDoc.test.ts +6 -15
  2. package/abstractDoc.ts +1 -4
  3. package/agentsIndex.test.ts +4 -7
  4. package/agentsIndex.ts +6 -22
  5. package/akanApp/BackendImportGraph.test.ts +8 -22
  6. package/akanApp/BackendImportGraph.ts +1 -20
  7. package/akanApp/akanApp.host.test.ts +1 -3
  8. package/akanApp/akanApp.host.ts +64 -250
  9. package/akanApp/devHostPolicy.test.ts +10 -34
  10. package/akanApp/devHostPolicy.ts +12 -98
  11. package/akanConfig/akanConfig.test.ts +30 -39
  12. package/akanConfig/akanConfig.ts +40 -136
  13. package/akanContext.ts +106 -159
  14. package/akanMcpContract.ts +28 -73
  15. package/appSelectionMemory.ts +1 -4
  16. package/applicationBuildReporter.ts +37 -5
  17. package/applicationBuildRunner.ts +25 -59
  18. package/applicationReleasePackager.ts +24 -44
  19. package/applicationTestPreload.ts +3 -12
  20. package/artifact/implicitRootLayout.test.ts +3 -19
  21. package/artifact/implicitRootLayout.ts +34 -91
  22. package/artifact/routeSeedIndex.ts +11 -44
  23. package/biomeBase.ts +2 -5
  24. package/capacitorApp.test.ts +4 -14
  25. package/capacitorApp.ts +86 -235
  26. package/cloud/cloudApi.test.ts +202 -0
  27. package/cloud/cloudApi.ts +33 -57
  28. package/cloud/configLock.test.ts +64 -0
  29. package/cloud/configLock.ts +96 -0
  30. package/cloud/constants.ts +3 -2
  31. package/cloud/globalConfig.ts +64 -31
  32. package/codeAgent/agent/AkanCodeServices.ts +15 -57
  33. package/codeAgent/agent/AkanContextFiles.ts +5 -28
  34. package/codeAgent/agent/CodeAgent.ts +33 -108
  35. package/codeAgent/agent/CodeAgentAsks.ts +7 -30
  36. package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
  37. package/codeAgent/agent/CodeAgentGate.ts +8 -34
  38. package/codeAgent/agent/CodeAgentProxy.ts +4 -13
  39. package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
  40. package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
  41. package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
  42. package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
  43. package/codeAgent/agent/CodeAgentUi.ts +2 -7
  44. package/codeAgent/agent/CodeMailbox.ts +7 -34
  45. package/codeAgent/agent/CodeSessionFork.ts +4 -19
  46. package/codeAgent/agent/CodeSessionIndex.ts +5 -31
  47. package/codeAgent/agent/SubagentPool.ts +48 -58
  48. package/codeAgent/agent/akanCodeModel.ts +5 -39
  49. package/codeAgent/agent/akanCodePaths.ts +4 -24
  50. package/codeAgent/agent/akanContextFiles.test.ts +0 -2
  51. package/codeAgent/agent/akanSystemPrompt.ts +1 -9
  52. package/codeAgent/agent/codeAgent.test.ts +178 -0
  53. package/codeAgent/agent/codeAgentContract.test.ts +5 -28
  54. package/codeAgent/agent/codeMailbox.test.ts +0 -4
  55. package/codeAgent/agent/codeSessionFork.test.ts +0 -2
  56. package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
  57. package/codeAgent/agent/consoleToStderr.ts +2 -12
  58. package/codeAgent/agent/jsonLines.ts +29 -0
  59. package/codeAgent/agent/stringArg.ts +5 -0
  60. package/codeAgent/feedback/DevLogFeedback.ts +3 -18
  61. package/codeAgent/feedback/PreviewChrome.ts +5 -29
  62. package/codeAgent/feedback/PreviewFeedback.ts +2 -9
  63. package/codeAgent/feedback/PreviewView.ts +3 -15
  64. package/codeAgent/feedback/TurnFeedback.ts +2 -17
  65. package/codeAgent/feedback/VerifyFeedback.ts +1 -7
  66. package/codeAgent/feedback/previewChrome.test.ts +1 -1
  67. package/codeAgent/index.ts +2 -10
  68. package/codeAgent/skills/akan-code/SKILL.md +12 -4
  69. package/codeAgent/tools/AkanCli.ts +4 -16
  70. package/codeAgent/tools/AkanCodePlugins.ts +21 -43
  71. package/codeAgent/tools/AkanEditScope.ts +2 -12
  72. package/codeAgent/tools/AkanEnvKeys.ts +3 -11
  73. package/codeAgent/tools/AkanToolPack.ts +6 -28
  74. package/codeAgent/tools/AkanVerifier.ts +1 -8
  75. package/codeAgent/tools/AskToolPack.ts +3 -15
  76. package/codeAgent/tools/MailToolPack.ts +0 -9
  77. package/codeAgent/tools/McpClient.ts +3 -24
  78. package/codeAgent/tools/McpOAuth.ts +6 -41
  79. package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
  80. package/codeAgent/tools/McpServerConfig.ts +7 -26
  81. package/codeAgent/tools/McpSignIn.ts +46 -49
  82. package/codeAgent/tools/McpTokenStore.ts +6 -17
  83. package/codeAgent/tools/McpToolPack.ts +107 -35
  84. package/codeAgent/tools/SessionToolPack.ts +1 -11
  85. package/codeAgent/tools/WebToolPack.ts +1 -7
  86. package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
  87. package/codeAgent/tools/askToolPack.test.ts +0 -3
  88. package/codeAgent/tools/mcpOAuth.test.ts +315 -27
  89. package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
  90. package/codegenLock.test.ts +4 -13
  91. package/codegenLock.ts +3 -17
  92. package/commandDecorators/argMeta.ts +1 -5
  93. package/commandDecorators/camelToKebabCase.ts +1 -0
  94. package/commandDecorators/command.ts +44 -102
  95. package/commandDecorators/commandBuilder.ts +30 -61
  96. package/commandDecorators/commandDecorators.test.ts +1 -9
  97. package/commandDecorators/dependencyBuilder.ts +10 -1
  98. package/commandDecorators/helpFormatter.ts +66 -144
  99. package/commandDecorators/index.ts +0 -2
  100. package/commandDecorators/targetMeta.ts +9 -7
  101. package/createTunnel.ts +7 -14
  102. package/dependencyScanner.test.ts +4 -16
  103. package/dependencyScanner.ts +27 -78
  104. package/devkitUtils.test.ts +47 -19
  105. package/executors.test.ts +114 -60
  106. package/executors.ts +165 -484
  107. package/fileEditor.ts +9 -37
  108. package/{getDirname.test.ts → fileSys.test.ts} +1 -1
  109. package/fileSys.ts +6 -8
  110. package/formSetterScanner.ts +3 -16
  111. package/frontendBuild/allRoutesBuilder.ts +7 -21
  112. package/frontendBuild/autoImportSync.test.ts +14 -38
  113. package/frontendBuild/autoImportSync.ts +15 -67
  114. package/frontendBuild/buildRouteClient.test.ts +10 -36
  115. package/frontendBuild/bundleDefine.ts +12 -0
  116. package/frontendBuild/clientBuildTypes.ts +5 -36
  117. package/frontendBuild/clientEntriesBundler.ts +9 -40
  118. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  119. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  120. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  121. package/frontendBuild/cssCandidateCache.ts +6 -41
  122. package/frontendBuild/cssCompiler.ts +29 -78
  123. package/frontendBuild/cssImportResolver.ts +5 -18
  124. package/frontendBuild/devChangePlanner.ts +7 -10
  125. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  126. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  127. package/frontendBuild/fontOptimizer.test.ts +5 -13
  128. package/frontendBuild/fontOptimizer.ts +15 -72
  129. package/frontendBuild/fontPruner.test.ts +5 -10
  130. package/frontendBuild/fontPruner.ts +5 -20
  131. package/frontendBuild/frontendBuild.test.ts +27 -52
  132. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  133. package/frontendBuild/hmrWatcher.test.ts +7 -31
  134. package/frontendBuild/hmrWatcher.ts +14 -71
  135. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  136. package/frontendBuild/precompressArtifacts.ts +5 -18
  137. package/frontendBuild/routeClientBuilder.ts +7 -47
  138. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  139. package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
  140. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  141. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  142. package/frontendBuild/styleContract.ts +1 -8
  143. package/frontendBuild/themeValidator.ts +4 -26
  144. package/frontendBuild/watchRootResolver.test.ts +23 -42
  145. package/frontendBuild/watchRootResolver.ts +5 -27
  146. package/getRelatedCnsts.ts +12 -92
  147. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  148. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  149. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  150. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  151. package/incrementalBuilder/builderChannel.test.ts +24 -49
  152. package/incrementalBuilder/builderChannel.ts +6 -37
  153. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  154. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  155. package/incrementalBuilder/devWatchBatch.test.ts +14 -18
  156. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  157. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  158. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  159. package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
  160. package/index.ts +3 -15
  161. package/integration/devResourceProbe.ts +50 -56
  162. package/integration/devStability.integration.test.ts +47 -194
  163. package/integration/devStabilityHarness.test.ts +9 -29
  164. package/integration/devStabilityHarness.ts +65 -330
  165. package/integration/ssrMemoryProbe.ts +41 -167
  166. package/libSource.test.ts +4 -15
  167. package/libSource.ts +19 -31
  168. package/lint/gritRules.test.ts +6 -23
  169. package/linter.test.ts +1 -2
  170. package/linter.ts +22 -115
  171. package/mobile/mobileTarget.ts +14 -13
  172. package/package.json +2 -2
  173. package/packageExports.test.ts +3 -18
  174. package/packageExportsMap.ts +3 -19
  175. package/piDependencyPins.test.ts +3 -14
  176. package/platformTest/IdleWatchedProcess.test.ts +5 -13
  177. package/platformTest/IdleWatchedProcess.ts +1 -5
  178. package/platformTest/LinuxDockerTarget.ts +20 -35
  179. package/platformTest/PlatformTestRun.test.ts +5 -9
  180. package/platformTest/PlatformTestTarget.ts +24 -2
  181. package/platformTest/TestSnapshot.test.ts +6 -14
  182. package/platformTest/TestSnapshot.ts +1 -5
  183. package/platformTest/WindowsSshTarget.ts +6 -22
  184. package/prompter.ts +3 -6
  185. package/qualityScanner.test.ts +6 -24
  186. package/qualityScanner.ts +196 -284
  187. package/recipeScanner.test.ts +7 -12
  188. package/recipeScanner.ts +9 -38
  189. package/repoIdentity.ts +2 -9
  190. package/routeSourceValidator.ts +7 -30
  191. package/scanInfo.ts +82 -214
  192. package/selectModel.ts +1 -2
  193. package/semver.ts +3 -5
  194. package/slicePlanner.test.ts +6 -24
  195. package/slicePlanner.ts +11 -32
  196. package/spinner.test.ts +3 -8
  197. package/spinner.ts +4 -16
  198. package/sshTunnel.ts +1 -6
  199. package/ssrScanner.test.ts +1 -6
  200. package/ssrScanner.ts +103 -113
  201. package/subspace.test.ts +6 -25
  202. package/subspace.ts +31 -109
  203. package/subspaceConfig.ts +3 -11
  204. package/testHelpers.ts +49 -34
  205. package/transforms/asyncDefaultExportDetector.ts +3 -11
  206. package/transforms/barrelAnalyzer.ts +43 -135
  207. package/transforms/barrelImportsPlugin.ts +38 -175
  208. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  209. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  210. package/transforms/moduleSyntax.ts +32 -0
  211. package/transforms/rscUseClientTransform.ts +35 -20
  212. package/transforms/transforms.test.ts +153 -44
  213. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  214. package/transforms/useClientBundlePlugin.ts +2 -22
  215. package/typeChecker.ts +7 -63
  216. package/uploadRelease.ts +10 -25
  217. package/workflow/artifacts.ts +18 -0
  218. package/workflow/executor.test.ts +1 -12
  219. package/workflow/executor.ts +85 -146
  220. package/workflow/index.ts +0 -1
  221. package/workflow/moduleIndex.test.ts +4 -15
  222. package/workflow/moduleIndex.ts +9 -49
  223. package/workflow/plan.ts +18 -25
  224. package/workflow/render.ts +80 -96
  225. package/workflow/source.ts +33 -56
  226. package/workflow/sourceAst.ts +50 -0
  227. package/workspaceLayout.ts +4 -19
  228. package/commandDecorators/commandMeta.ts +0 -7
  229. package/commandDecorators/types.ts +0 -10
  230. package/getDirname.ts +0 -4
  231. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  232. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  233. 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
- * Sends every `console.*` call in this process to stderr, so stdout carries the RPC protocol and nothing else.
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
+ };
@@ -0,0 +1,5 @@
1
+ export const stringArg = (args: unknown, key: "path" | "command") => {
2
+ if (!args || typeof args !== "object") return undefined;
3
+ const value = (args as Record<string, unknown>)[key];
4
+ return typeof value === "string" && value ? value : undefined;
5
+ };
@@ -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 running app. Without this, one line from a dev server
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 one attached; a screenshot is ~83KB per shot. */
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
- * Opens a page headlessly and reports what is wrong with it.
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
- * Closes the loop between what a turn did and what the agent knows about it.
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
- // Leaving the apt lists behind adds ~40MB to every layer of every image that takes this step.
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
  });
@@ -1,13 +1,5 @@
1
- /**
2
- * The akan coding agent: the engine wrapper, the tools it is given, and the feedback that reopens a turn.
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 **`.akan/code/mcp.json`**, in the same `mcpServers` shape Claude, Cursor and VS Code already
19
- write, so a block can be pasted across rather than translated. VS Code's `servers` spelling is read too, and
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 remove <name>
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; `/mcp` shows `sign-in needed` when a server wants one.
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
- // `NO_COLOR` reaches akan's own output but not every tool it shells out to — tsgo colours regardless, and
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
- * Bundling rewrites `import.meta.dir` to the output directory, where devkit is inlined into the CLI and the
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 askExtension = ask?.extension();
76
- if (!options.profile.tools.akan)
77
- return {
78
- extensions: [mcp?.extension(), webExtension, sessionExtension, mailExtension, askExtension].filter(
79
- (entry) => !!entry,
80
- ),
81
- toolNames: [
82
- ...(mcp?.toolNames ?? []),
83
- ...web.names(),
84
- ...sessions.names(),
85
- ...mail.names(),
86
- ...(ask?.names() ?? []),
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
- // A dev server is up and the agent still cannot look at the page. Said once, at assembly, because the
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.#status(cwd);
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",