@akanjs/devkit 3.0.0-alpha.7 → 3.0.0-alpha.9

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/agentsIndex.ts CHANGED
@@ -19,6 +19,32 @@ const loadRecipeScanner = () => (recipeScannerLoad ??= import("./recipeScanner")
19
19
  export const AGENT_BLOCK_START = "<!-- akan:agent:start -->";
20
20
  export const AGENT_BLOCK_END = "<!-- akan:agent:end -->";
21
21
 
22
+ const AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
23
+
24
+ /**
25
+ * The generating release, stamped into the workspace block. Nothing re-runs `akan agent install` on its own, so
26
+ * without a stamp a workspace carrying conventions from four releases ago is indistinguishable from a current one —
27
+ * `akan doctor` compares this against the installed devkit and says so.
28
+ */
29
+ export const stampBlockVersion = (block: string, version: string): string =>
30
+ `${AGENT_VERSION_PREFIX}${version} -->\n\n${block}`;
31
+
32
+ /** The stamped version of a committed AGENTS.md, or null when it predates stamping or carries no block. */
33
+ export const extractBlockVersion = (content: string): string | null =>
34
+ content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
35
+
36
+ /** The running `@akanjs/devkit` version; null when its package.json is unreadable, which must not fail a doctor run. */
37
+ export const readDevkitVersion = async (): Promise<string | null> => {
38
+ const { readFile } = await import("node:fs/promises");
39
+ const { getDirname } = await import("./getDirname");
40
+ try {
41
+ const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
42
+ return (JSON.parse(raw) as { version?: string }).version ?? null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ };
47
+
22
48
  export interface AgentsIndexScope {
23
49
  type: "app" | "lib";
24
50
  name: string;
package/akanContext.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { capitalize } from "akanjs/common";
4
+ import { extractBlockVersion, readDevkitVersion } from "./agentsIndex";
4
5
  import { AppExecutor, LibExecutor, type SysExecutor, type WorkspaceExecutor } from "./executors";
5
6
  import { FileSys } from "./fileSys";
6
7
  import { collectRecipeSources, findInlineRecipeDuplicates, scanRecipes } from "./recipeScanner";
@@ -276,6 +277,8 @@ const validationCommands = [
276
277
  "akan test <app-or-lib-or-pkg>",
277
278
  "akan build <app-name>",
278
279
  "akan doctor --strict --format json",
280
+ "akan quality scan [--format json]",
281
+ "akan quality ssr [--format json]",
279
282
  ];
280
283
 
281
284
  const unknownGeneratedFilesFreshness: GeneratedFilesFreshness = {
@@ -618,6 +621,30 @@ export class AkanContextAnalyzer {
618
621
  };
619
622
  }
620
623
 
624
+ // The conventions in AGENTS.md ship with the package, but nothing re-renders them on `bun update` — a workspace
625
+ // keeps whichever release wrote its block until someone re-runs the install. Comparing the stamp against the
626
+ // running devkit is the only signal that the guide an agent is reading is older than the framework it describes.
627
+ static async #agentGuideDrift(workspace: WorkspaceExecutor) {
628
+ const installed = await readDevkitVersion();
629
+ if (!installed) return null;
630
+ const content = await safeReadText(path.join(workspace.workspaceRoot, "AGENTS.md"));
631
+ if (content === null) return null;
632
+ const stamped = extractBlockVersion(content);
633
+ if (stamped === installed) return null;
634
+ const hint = "Re-render the AGENTS.md managed block from the installed framework release.";
635
+ if (!stamped)
636
+ return {
637
+ code: "agent-guide-unstamped",
638
+ message: `AGENTS.md carries no generated-version stamp, so its conventions may predate @akanjs/devkit ${installed}`,
639
+ hint,
640
+ };
641
+ return {
642
+ code: "agent-guide-stale",
643
+ message: `AGENTS.md was generated by @akanjs/devkit ${stamped}, but ${installed} is installed`,
644
+ hint,
645
+ };
646
+ }
647
+
621
648
  static async doctor(
622
649
  workspace: WorkspaceExecutor,
623
650
  {
@@ -663,6 +690,19 @@ export class AkanContextAnalyzer {
663
690
  }
664
691
  }
665
692
 
693
+ const agentDrift = await AkanContextAnalyzer.#agentGuideDrift(workspace);
694
+ if (agentDrift) {
695
+ const action = repairAction("generated", "akan agent install agents-md", agentDrift.hint, true);
696
+ diagnostics.push({
697
+ severity: "warning",
698
+ code: agentDrift.code,
699
+ path: "AGENTS.md",
700
+ message: agentDrift.message,
701
+ repairActions: [action],
702
+ });
703
+ repairActions.push(action);
704
+ }
705
+
666
706
  for (const sys of [...context.apps, ...context.libs]) {
667
707
  for (const module of sys.modules) {
668
708
  if (!module.abstract.exists) {
@@ -0,0 +1,301 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": false
7
+ },
8
+ "formatter": {
9
+ "enabled": true,
10
+ "indentStyle": "space",
11
+ "indentWidth": 2,
12
+ "lineWidth": 120
13
+ },
14
+ "linter": {
15
+ "enabled": true,
16
+ "rules": {
17
+ "recommended": true,
18
+ "suspicious": {
19
+ "noConsole": {
20
+ "level": "error",
21
+ "options": {
22
+ "allow": ["assert", "error", "info", "warn"]
23
+ }
24
+ },
25
+ "noArrayIndexKey": "off",
26
+ "noShadowRestrictedNames": "off"
27
+ },
28
+ "correctness": {
29
+ "noUnusedFunctionParameters": "off",
30
+ "noUnusedImports": {
31
+ "level": "error",
32
+ "fix": "safe"
33
+ },
34
+ "useParseIntRadix": "off",
35
+ "useExhaustiveDependencies": "off",
36
+ "useHookAtTopLevel": "off"
37
+ },
38
+ "nursery": {
39
+ "useSortedClasses": {
40
+ "level": "error",
41
+ "fix": "safe",
42
+ "options": {
43
+ "attributes": ["classList"],
44
+ "functions": ["cn"]
45
+ }
46
+ },
47
+ "noUnnecessaryConditions": {
48
+ "level": "error"
49
+ }
50
+ },
51
+ "a11y": "off",
52
+ "complexity": {
53
+ "noStaticOnlyClass": "off"
54
+ },
55
+ "style": {
56
+ "useTemplate": {
57
+ "level": "warn",
58
+ "fix": "safe"
59
+ }
60
+ }
61
+ },
62
+ "domains": {
63
+ "project": "recommended",
64
+ "react": "recommended",
65
+ "test": "recommended",
66
+ "types": "all"
67
+ }
68
+ },
69
+ "javascript": {
70
+ "parser": {
71
+ "unsafeParameterDecoratorsEnabled": true
72
+ },
73
+ "formatter": {
74
+ "quoteStyle": "double"
75
+ }
76
+ },
77
+ "assist": {
78
+ "enabled": true,
79
+ "actions": {
80
+ "source": {
81
+ "organizeImports": "on"
82
+ }
83
+ }
84
+ },
85
+ "overrides": [
86
+ {
87
+ "includes": [
88
+ "**/env.client.local.ts",
89
+ "**/env.client.debug.ts",
90
+ "**/env.client.develop.ts",
91
+ "**/env.client.main.ts",
92
+ "**/env.client.testing.ts",
93
+ "**/env.server.local.ts",
94
+ "**/env.server.develop.ts",
95
+ "**/env.server.main.ts",
96
+ "**/env.server.testing.ts",
97
+ "**/env.server.type.ts",
98
+ "apps/*/lib/cnst.ts",
99
+ "apps/*/lib/dict.ts",
100
+ "apps/*/lib/db.ts",
101
+ "apps/*/lib/srv.ts",
102
+ "apps/*/lib/st.ts",
103
+ "apps/*/lib/sig.ts",
104
+ "apps/*/lib/useClient.ts",
105
+ "apps/*/lib/useServer.ts",
106
+ "apps/*/client.ts",
107
+ "apps/*/server.ts",
108
+ "apps/*/lib/*/index.tsx",
109
+ "libs/*/lib/cnst.ts",
110
+ "libs/*/lib/dict.ts",
111
+ "libs/*/lib/db.ts",
112
+ "libs/*/lib/srv.ts",
113
+ "libs/*/lib/st.ts",
114
+ "libs/*/lib/sig.ts",
115
+ "libs/*/lib/useClient.ts",
116
+ "libs/*/lib/useServer.ts",
117
+ "libs/*/client.ts",
118
+ "libs/*/server.ts",
119
+ "libs/*/index.ts",
120
+ "libs/*/lib/*/index.tsx"
121
+ ],
122
+ "linter": { "enabled": false },
123
+ "formatter": { "enabled": false },
124
+ "assist": { "enabled": false }
125
+ },
126
+ {
127
+ "includes": [
128
+ "apps/**/*.ts",
129
+ "apps/**/*.tsx",
130
+ "libs/**/*.ts",
131
+ "libs/**/*.tsx",
132
+ "!**/*.test.ts",
133
+ "!**/*.test.tsx",
134
+ "!**/*.spec.ts",
135
+ "!**/*.spec.tsx",
136
+ "!**/*.constant.ts",
137
+ "!**/common/**",
138
+ "!**/env/**"
139
+ ],
140
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-throw-raw-error.grit"]
141
+ },
142
+ {
143
+ "includes": ["**/page/**/*.ts", "**/page/**/*.tsx", "**/*.Unit.tsx", "**/*.View.tsx"],
144
+ "plugins": [
145
+ "./node_modules/@akanjs/devkit/lint/no-import-client-functions.grit",
146
+ "./node_modules/@akanjs/devkit/lint/no-use-client-in-server.grit"
147
+ ]
148
+ },
149
+ {
150
+ "includes": ["**/page/**/*.ts", "**/page/**/*.tsx"],
151
+ "plugins": ["./node_modules/@akanjs/devkit/lint/non-scalar-props-restricted.grit"]
152
+ },
153
+ {
154
+ "includes": ["**/*.constant.ts", "**/*.document.ts", "**/*.service.ts", "**/*.store.ts"],
155
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-js-private-class-method.grit"]
156
+ },
157
+ {
158
+ "includes": ["**/*.store.ts"],
159
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-return-in-store-action.grit"]
160
+ },
161
+ {
162
+ "includes": ["**/*.signal.ts"],
163
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-redeclare-predefined-endpoint.grit"]
164
+ },
165
+ {
166
+ "includes": [
167
+ "**/ui/**/*.ts",
168
+ "**/ui/**/*.tsx",
169
+ "**/webkit/**/*.ts",
170
+ "**/webkit/**/*.tsx",
171
+ "**/common/**/*.ts",
172
+ "**/page/**/*.tsx",
173
+ "**/*.constant.ts",
174
+ "**/*.store.ts",
175
+ "**/*.Template.tsx",
176
+ "**/*.Unit.tsx",
177
+ "**/*.Util.tsx",
178
+ "**/*.View.tsx",
179
+ "**/*.Zone.tsx",
180
+ "!**/*.test.ts",
181
+ "!**/*.test.tsx",
182
+ "!**/*.spec.ts",
183
+ "!**/*.spec.tsx"
184
+ ],
185
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-bang-comment-in-client.grit"]
186
+ },
187
+ {
188
+ "includes": [
189
+ "**/*.constant.ts",
190
+ "**/*.dictionary.ts",
191
+ "**/*.document.ts",
192
+ "**/*.service.ts",
193
+ "**/*.signal.ts",
194
+ "**/*.store.ts",
195
+ "**/*.Template.tsx",
196
+ "**/*.Unit.tsx",
197
+ "**/*.Util.tsx",
198
+ "**/*.View.tsx",
199
+ "**/*.Zone.tsx"
200
+ ],
201
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-deep-internal-import.grit"]
202
+ },
203
+ {
204
+ "includes": [
205
+ "**/page/**/*.ts",
206
+ "**/page/**/*.tsx",
207
+ "**/index.ts",
208
+ "**/index.tsx",
209
+ "**/cnst.ts",
210
+ "**/db.ts",
211
+ "**/dict.ts",
212
+ "**/option.ts",
213
+ "**/sig.ts",
214
+ "**/srv.ts",
215
+ "**/st.ts",
216
+ "**/*.constant.ts",
217
+ "**/*.dictionary.ts",
218
+ "**/*.document.ts",
219
+ "**/*.service.ts",
220
+ "**/*.signal.ts",
221
+ "**/*.signal.test.ts",
222
+ "**/*.service.test.ts",
223
+ "**/*.store.ts",
224
+ "**/*.Template.tsx",
225
+ "**/*.Unit.tsx",
226
+ "**/*.Util.tsx",
227
+ "**/*.View.tsx",
228
+ "**/*.Zone.tsx"
229
+ ],
230
+ "plugins": [
231
+ "./node_modules/@akanjs/devkit/lint/no-import-external-library.grit",
232
+ "./node_modules/@akanjs/devkit/lint/no-deep-internal-import.grit"
233
+ ]
234
+ },
235
+ {
236
+ "includes": [
237
+ "apps/**/*.tsx",
238
+ "libs/**/*.tsx",
239
+ "apps/**/ui/**/*.ts",
240
+ "libs/**/ui/**/*.ts",
241
+ "apps/**/webkit/**/*.ts",
242
+ "libs/**/webkit/**/*.ts",
243
+ "apps/**/page/**/*.ts",
244
+ "libs/**/page/**/*.ts",
245
+ "apps/**/common/**/*.ts",
246
+ "libs/**/common/**/*.ts",
247
+ "apps/**/*.store.ts",
248
+ "libs/**/*.store.ts",
249
+ "apps/**/*.constant.ts",
250
+ "libs/**/*.constant.ts",
251
+ "!**/*.test.ts",
252
+ "!**/*.test.tsx",
253
+ "!**/*.spec.ts",
254
+ "!**/*.spec.tsx"
255
+ ],
256
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-import-server-in-client.grit"]
257
+ },
258
+ {
259
+ "includes": [
260
+ "apps/**/*.document.ts",
261
+ "libs/**/*.document.ts",
262
+ "apps/**/*.dictionary.ts",
263
+ "libs/**/*.dictionary.ts",
264
+ "apps/**/*.service.ts",
265
+ "libs/**/*.service.ts",
266
+ "apps/**/*.signal.ts",
267
+ "libs/**/*.signal.ts",
268
+ "apps/**/srvkit/**/*.ts",
269
+ "libs/**/srvkit/**/*.ts",
270
+ "apps/**/common/**/*.ts",
271
+ "libs/**/common/**/*.ts",
272
+ "apps/**/*.constant.ts",
273
+ "libs/**/*.constant.ts",
274
+ "!**/*.test.ts",
275
+ "!**/*.test.tsx",
276
+ "!**/*.spec.ts",
277
+ "!**/*.spec.tsx"
278
+ ],
279
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-import-client-in-server.grit"]
280
+ },
281
+ {
282
+ "includes": [
283
+ "apps/**/*.ts",
284
+ "apps/**/*.tsx",
285
+ "libs/**/*.ts",
286
+ "libs/**/*.tsx",
287
+ "!**/*.test.ts",
288
+ "!**/*.test.tsx",
289
+ "!**/*.spec.ts",
290
+ "!**/*.spec.tsx"
291
+ ],
292
+ "plugins": [
293
+ "./node_modules/@akanjs/devkit/lint/no-raw-palette-class.grit",
294
+ "./node_modules/@akanjs/devkit/lint/no-arbitrary-color.grit",
295
+ "./node_modules/@akanjs/devkit/lint/no-daisyui-legacy-class.grit",
296
+ "./node_modules/@akanjs/devkit/lint/no-inline-color.grit",
297
+ "./node_modules/@akanjs/devkit/lint/no-interpolated-arbitrary-class.grit"
298
+ ]
299
+ }
300
+ ]
301
+ }
package/biomeBase.ts ADDED
@@ -0,0 +1,8 @@
1
+ /** `extends` target for a workspace `biome.json`; Biome resolves it through node_modules. */
2
+ export const biomeBaseConfig = "@akanjs/devkit/biome.base.json";
3
+
4
+ // Biome moves rules between groups across minors — `noUnnecessaryConditions` sits in `nursery` at 2.4 and in
5
+ // `suspicious` from 2.5, and the old position is a hard "unknown key" error, not a warning. A workspace one minor
6
+ // ahead of the shipped base config therefore fails to load it at all, so the version is pinned rather than resolved
7
+ // to latest at create time. Bump this and `biome.base.json` in the same commit.
8
+ export const biomeVersion = "2.4.4";
package/executors.test.ts CHANGED
@@ -149,8 +149,10 @@ describe("Executor filesystem helpers", () => {
149
149
  "AI Development Guide",
150
150
  );
151
151
  expect(await readFile(path.join(root, "workspace/docs/GENERATED.md"), "utf8")).toContain("Generated Akan Files");
152
+ // Rules and their plugin registrations live in the package's base config, so a framework release reaches an
153
+ // existing workspace on `bun update` — the workspace file only extends it and scopes its own files.
152
154
  expect(await readFile(path.join(root, "workspace/biome.json"), "utf8")).toContain(
153
- "./node_modules/@akanjs/devkit/lint/no-import-client-functions.grit",
155
+ '"extends": ["@akanjs/devkit/biome.base.json"]',
154
156
  );
155
157
  });
156
158
 
package/executors.ts CHANGED
@@ -42,6 +42,7 @@ import { AkanAppConfig, AkanLibConfig, decreaseBuildNum, increaseBuildNum } from
42
42
  import { FileSys } from "./fileSys";
43
43
  import { getDirname } from "./getDirname";
44
44
  import { Linter } from "./linter";
45
+ import { resolveRepoName } from "./repoIdentity";
45
46
  import { AppInfo, LibInfo, PkgInfo, WorkspaceInfo } from "./scanInfo";
46
47
  import { Spinner } from "./spinner";
47
48
  // Type-only: the implementation is loaded on demand in `getTypeChecker` to keep `typescript` out of
@@ -788,7 +789,7 @@ export class WorkspaceExecutor extends Executor {
788
789
  static #execs = new Map<string, WorkspaceExecutor>();
789
790
  static fromRoot({
790
791
  workspaceRoot = process.cwd(),
791
- repoName = path.basename(process.cwd()),
792
+ repoName = resolveRepoName(workspaceRoot),
792
793
  }: {
793
794
  workspaceRoot?: string;
794
795
  repoName?: string;
@@ -0,0 +1,48 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // Server files (*.document.ts, *.dictionary.ts, *.service.ts, *.signal.ts, srvkit/) and shared files
5
+ // (common/, *.constant.ts) run in Bun with no DOM and no bundler, and every CLI command, worker, and
6
+ // migration that loads a service loads whatever the service imports. Reaching into the client graph
7
+ // pulls React, the store, and the browser globals they touch into that process, and it runs the
8
+ // dependency backwards: the client entrypoint is built on top of cnst and sig, not the other way round.
9
+ //
10
+ // `import type` is erased before bundling and stays legal. A mixed value-and-type import is not exempt.
11
+ or {
12
+ JsModuleSource() as $source where {
13
+ $source <: within JsImport() as $import,
14
+ not $import <: r"import\s+type[\s\S]*",
15
+ $source <: or {
16
+ r"\".*\.store\"",
17
+ r"\".*\.(?:Template|Unit|Util|View|Zone)\""
18
+ },
19
+ register_diagnostic(
20
+ span = $source,
21
+ message = "Client module. A server or shared file must not import a *.store or a module component (*.Template, *.Unit, *.Util, *.View, *.Zone) — server code reaches the model through cnst and db, never through client state or JSX.",
22
+ severity = "error"
23
+ )
24
+ },
25
+ JsModuleSource() as $source where {
26
+ $source <: within JsImport() as $import,
27
+ not $import <: r"import\s+type[\s\S]*",
28
+ $source <: or {
29
+ r"\"(?:.*/)?(?:ui|webkit)(?:/.*)?\"",
30
+ r"\".*/client(?:/.*)?\""
31
+ },
32
+ register_diagnostic(
33
+ span = $source,
34
+ message = "Client entrypoint. A server or shared file must not import ui/, webkit/, or a package client entrypoint such as '@libs/<lib>/client' or 'akanjs/client' — import the server entrypoint or a common/ helper instead.",
35
+ severity = "error"
36
+ )
37
+ },
38
+ JsModuleSource() as $source where {
39
+ $source <: within JsImport() as $import,
40
+ not $import <: r"import\s+type[\s\S]*",
41
+ $source <: r"\"(?:.*/)?(?:st|store|useClient)\"",
42
+ register_diagnostic(
43
+ span = $source,
44
+ message = "Client barrel. A server or shared file must not import st, store, or useClient — server code holds no client state, and Err comes from dict on the server.",
45
+ severity = "error"
46
+ )
47
+ }
48
+ }
@@ -0,0 +1,45 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // Client files (ui/, webkit/, page/, *.store.ts, every .tsx) and shared files (common/, *.constant.ts)
5
+ // are compiled into the browser bundle, so a single value import of a server module drags its whole
6
+ // graph along — the database driver, node:crypto, a secret resolved from process.env. The boundary has
7
+ // to hold at the import statement, because by the call site the module is already bundled.
8
+ //
9
+ // `import type` is erased before bundling and stays legal, so a shared file may still name a server-side
10
+ // type. A mixed value-and-type import is not exempt: it emits a real edge.
11
+ or {
12
+ JsModuleSource() as $source where {
13
+ $source <: within JsImport() as $import,
14
+ not $import <: r"import\s+type[\s\S]*",
15
+ $source <: r"\".*\.(?:document|dictionary|service|signal)\"",
16
+ register_diagnostic(
17
+ span = $source,
18
+ message = "Server module. A client or shared file must not import a *.document, *.dictionary, *.service, or *.signal file — take the model from the package client entrypoint ('@libs/<lib>/client') or from its *.constant instead. Write 'import type' if only the type is needed.",
19
+ severity = "error"
20
+ )
21
+ },
22
+ JsModuleSource() as $source where {
23
+ $source <: within JsImport() as $import,
24
+ not $import <: r"import\s+type[\s\S]*",
25
+ $source <: or {
26
+ r"\"(?:.*/)?srvkit(?:/.*)?\"",
27
+ r"\".*/server(?:/.*)?\""
28
+ },
29
+ register_diagnostic(
30
+ span = $source,
31
+ message = "Server entrypoint. A client or shared file must not import srvkit/ or a package server entrypoint such as '@apps/<app>/server' or 'akanjs/server' — import the matching client entrypoint instead.",
32
+ severity = "error"
33
+ )
34
+ },
35
+ JsModuleSource() as $source where {
36
+ $source <: within JsImport() as $import,
37
+ not $import <: r"import\s+type[\s\S]*",
38
+ $source <: r"\"(?:.*/)?(?:db|srv|sig|dict|option|useServer)\"",
39
+ register_diagnostic(
40
+ span = $source,
41
+ message = "Server barrel. A client or shared file must not import db, srv, sig, dict, option, or useServer — read models from cnst, state from st, and Err from the package client entrypoint.",
42
+ severity = "error"
43
+ )
44
+ }
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "3.0.0-alpha.7",
3
+ "version": "3.0.0-alpha.9",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -23,6 +23,7 @@
23
23
  "default": "./index.ts"
24
24
  },
25
25
  "./package.json": "./package.json",
26
+ "./biome.base.json": "./biome.base.json",
26
27
  "./akanApp": "./akanApp/index.ts",
27
28
  "./akanConfig": "./akanConfig/index.ts",
28
29
  "./artifact": "./artifact/index.ts",
@@ -44,7 +45,7 @@
44
45
  "@langchain/openai": "^1.4.6",
45
46
  "@tailwindcss/node": "^4.3.0",
46
47
  "@trapezedev/project": "^7.1.4",
47
- "akanjs": "3.0.0-alpha.7",
48
+ "akanjs": "3.0.0-alpha.9",
48
49
  "chalk": "^5.6.2",
49
50
  "commander": "^14.0.3",
50
51
  "dayjs": "^1.11.20",
@@ -0,0 +1,42 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import path from "node:path";
3
+
4
+ // Resolved once per root: every CLI command builds a WorkspaceExecutor, and this would otherwise fork git on each.
5
+ const resolved = new Map<string, string>();
6
+
7
+ const readRemoteName = (workspaceRoot: string): string | null => {
8
+ try {
9
+ const url = execFileSync("git", ["config", "--get", "remote.origin.url"], {
10
+ cwd: workspaceRoot,
11
+ encoding: "utf-8",
12
+ stdio: ["ignore", "pipe", "ignore"],
13
+ }).trim();
14
+ // Both remote spellings end in the repository: git@host:owner/name.git and https://host/owner/name.git.
15
+ return (
16
+ url
17
+ .replace(/\.git$/, "")
18
+ .split(/[/:]/)
19
+ .pop() || null
20
+ );
21
+ } catch {
22
+ // No git, no origin, or no git binary — a fresh `akan workspace` before its first commit lands here.
23
+ return null;
24
+ }
25
+ };
26
+
27
+ /**
28
+ * The repository's own name.
29
+ *
30
+ * Deriving it from the working directory made every generated file that names the repo — the AGENTS.md title, its
31
+ * `- Repo:` line — depend on what each person happened to call the folder they cloned into, so one commit rendered
32
+ * a different guide per developer and the diff never settled. The origin remote is the one identity every clone
33
+ * shares. `AKAN_PUBLIC_REPO_NAME` is deliberately not consulted: that is a deployment namespace (queue prefixes,
34
+ * cache keys, secret paths) which a monorepo hosting several products legitimately points somewhere else.
35
+ */
36
+ export const resolveRepoName = (workspaceRoot: string): string => {
37
+ const cached = resolved.get(workspaceRoot);
38
+ if (cached) return cached;
39
+ const repoName = readRemoteName(workspaceRoot) ?? path.basename(workspaceRoot);
40
+ resolved.set(workspaceRoot, repoName);
41
+ return repoName;
42
+ };
@@ -4,8 +4,7 @@
4
4
  * 같은 규칙을 scanInfo(`akan sync`, hard error) · akanContext(`akan doctor`, diagnostic) ·
5
5
  * qualityScanner(`akan quality scan`, warning) 세 곳이 각자 복사해 두면서 실제로 어긋났다
6
6
  * (스코프 AGENTS.md/CLAUDE.md 는 sync 만 허용, `plugin` 은 문서에만, `secrets` 는 doctor 만 거부).
7
- * 규칙을 추가할 때는 이 파일만 고치고, 루트 AGENTS.md `.cursor/rules/akan-scan-conventions.mdc`
8
- * 의 목록도 같이 갱신한다.
7
+ * 규칙을 추가할 때는 이 파일만 고치고, 루트 AGENTS.md 목록도 같이 갱신한다.
9
8
  */
10
9
 
11
10
  export const appRootAllowedFiles = new Set([