ark-runtime-kernel 1.2.0 → 1.3.0

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/README.md CHANGED
@@ -34,6 +34,15 @@ npx ark init # asks before generating config, agent gates, and
34
34
  npx ark-check # done: cross-layer imports now fail the check
35
35
  ```
36
36
 
37
+ `ark init` detects your existing layer directories and suggests the missing ones from
38
+ Ark's default 11-layer profile (with their conventional directories), so you see the
39
+ full division before deciding what to adopt. On an empty project it generates the
40
+ complete profile with every layer optional: the check passes immediately, and each
41
+ layer starts being enforced as soon as its directory gains source files. Agents get
42
+ the same guidance — the `ark://manifest` resource includes `suggestedLayers`, and the
43
+ generated `AGENTS.md` carries the placement table, so an agent asked for a saga or a
44
+ background job knows where it belongs before writing it.
45
+
37
46
  Adopting on a codebase that already has violations? Freeze them and ratchet down:
38
47
 
39
48
  ```bash
@@ -41,7 +50,9 @@ npx ark-check --update-baseline # writes .ark-baseline.json — commit it
41
50
  npx ark-check --baseline # only NEW violations fail from now on
42
51
  ```
43
52
 
44
- Then gate your agents (Claude Code shown; [Cursor / Codex / others](docs/ai-gates.md)):
53
+ Then gate your agents (Claude Code shown; [Cursor / Codex / others](docs/ai-gates.md)). If you use
54
+ Codex in an Ark project, register the MCP server early so `ark://manifest` is available during
55
+ generation:
45
56
 
46
57
  ```json
47
58
  // .claude/settings.json
@@ -71,6 +82,39 @@ GitHub Actions, and agent instructions. Existing files are skipped unless you pa
71
82
  The package `postinstall` only prints the next command; it never prompts or writes files
72
83
  during `npm install`. Use `npx ark init --yes` for non-interactive setup.
73
84
 
85
+ ### Updating Ark
86
+
87
+ For projects that already use Ark:
88
+
89
+ ```bash
90
+ npm install -D ark-runtime-kernel@latest
91
+ npx ark-check --root . --config ark.config.json --strict-config
92
+ npm run check:architecture
93
+ ```
94
+
95
+ This updates the local `ark`, `ark-check`, and `ark-mcp` binaries used by npm scripts
96
+ and CI. `npm run check:architecture` is the recommended alias, but it is optional:
97
+ the direct `npx ark-check --root . --config ark.config.json --strict-config` command
98
+ is the real check and works even if the alias has not been added yet.
99
+
100
+ The lockfile controls the version CI gets, so commit the updated `package-lock.json`,
101
+ `pnpm-lock.yaml`, or `yarn.lock`.
102
+
103
+ Generated setup files are intentionally not rewritten during package updates:
104
+ `AGENTS.md`, MCP config, Claude/Cursor settings, Codex notes, and GitHub Actions
105
+ templates stay under your project's control. To add any new starter templates:
106
+
107
+ ```bash
108
+ npx ark-check --install-agent-gates
109
+ ```
110
+
111
+ Existing files are skipped. To regenerate them from the latest templates, review
112
+ your local changes first, then run:
113
+
114
+ ```bash
115
+ npx ark-check --install-agent-gates --force
116
+ ```
117
+
74
118
  ## Why Ark (and not just a linter)?
75
119
 
76
120
  If you only need import-boundary linting in CI, [dependency-cruiser](https://github.com/sverweij/dependency-cruiser), [eslint-plugin-boundaries](https://github.com/javierbrea/eslint-plugin-boundaries), and Nx module boundaries are solid tools. Ark's reason to exist is the **write-time, agent-native half** they don't cover:
package/bin/ark-check.mjs CHANGED
@@ -21,8 +21,10 @@ function parseArgs(argv) {
21
21
  tsconfig: undefined,
22
22
  json: false,
23
23
  strictConfig: false,
24
+ requireGates: false,
24
25
  init: false,
25
26
  installAgentGates: false,
27
+ tools: undefined,
26
28
  force: false,
27
29
  baseline: undefined,
28
30
  updateBaseline: false,
@@ -31,8 +33,23 @@ function parseArgs(argv) {
31
33
  const arg = argv[i];
32
34
  if (arg === '--json') args.json = true;
33
35
  else if (arg === '--strict-config') args.strictConfig = true;
36
+ else if (arg === '--require-gates') args.requireGates = true;
34
37
  else if (arg === '--init') args.init = true;
35
38
  else if (arg === '--install-agent-gates') args.installAgentGates = true;
39
+ else if (arg === '--tools') {
40
+ // Consume the next arg only when it isn't another flag (same rule as --baseline),
41
+ // so `--tools --force` can't silently eat --force as a "tool name".
42
+ const next = argv[i + 1];
43
+ if (next !== undefined && !next.startsWith('-')) {
44
+ i += 1;
45
+ args.tools = next
46
+ .split(',')
47
+ .map((tool) => tool.trim().toLowerCase())
48
+ .filter(Boolean);
49
+ } else {
50
+ args.tools = []; // flag without a value — rejected in runInstallAgentGates
51
+ }
52
+ }
36
53
  else if (arg === '--force') args.force = true;
37
54
  else if (arg === '--baseline' || arg === '--update-baseline') {
38
55
  if (arg === '--update-baseline') args.updateBaseline = true;
@@ -52,9 +69,9 @@ function parseArgs(argv) {
52
69
 
53
70
  function usage() {
54
71
  return [
55
- 'Usage: ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-config] [--json] [--baseline [file]]',
72
+ 'Usage: ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-config] [--require-gates] [--json] [--baseline [file]]',
56
73
  ' ark-check --init [--force]',
57
- ' ark-check --install-agent-gates [--force]',
74
+ ' ark-check --install-agent-gates [--tools claude,cursor,codex] [--force]',
58
75
  ' ark-check --update-baseline [file] freeze current violations (default .ark-baseline.json)',
59
76
  ' ark-check --print-config eleven-layer',
60
77
  '',
@@ -65,6 +82,10 @@ function usage() {
65
82
  '--init scans the project for the built-in layer directory conventions (src/domain,',
66
83
  'src/application, src/adapters/persistence, ...) and writes an ark.config.json covering',
67
84
  'only the layers that actually exist, with the default rules filtered to those layers.',
85
+ 'Undetected profile layers are printed as suggestions with their conventional',
86
+ 'directories. When nothing is detected, the full 11-layer starter profile is written',
87
+ 'instead (all layers optional, anchored at src/), so the strict check passes today and',
88
+ 'each layer starts being enforced as soon as its directory gains source files.',
68
89
  '',
69
90
  'Resolves relative, tsconfig path-alias, and package imports via the TypeScript',
70
91
  'module resolver, then checks each resolved cross-layer import against the rules.',
@@ -83,6 +104,15 @@ function usage() {
83
104
  'Config warnings are advisory by default and are included in JSON output.',
84
105
  'Use --strict-config to make config warnings fail the check.',
85
106
  '',
107
+ '--require-gates fails the check when AGENTS.md, .mcp.json, or the generated CI',
108
+ 'workflow is missing, so "installed but never configured" is a red CI. Combine it',
109
+ 'with --strict-config to enforce gate presence and architecture in one run.',
110
+ '',
111
+ '--install-agent-gates writes AGENTS.md, .mcp.json, and the CI workflow for every',
112
+ 'project, plus tool-specific templates. Pass --tools claude,cursor,codex to pick',
113
+ 'which tool configs to write; otherwise they are auto-detected from .claude/, .cursor/,',
114
+ 'and .codex/ (all are written when nothing is detected).',
115
+ '',
86
116
  'Generate a starter 11-layer config:',
87
117
  ' ark-check --print-config eleven-layer > ark.config.json',
88
118
  '',
@@ -95,6 +125,35 @@ function readJson(file) {
95
125
  return JSON.parse(fs.readFileSync(file, 'utf8'));
96
126
  }
97
127
 
128
+ function readPackageJson(root) {
129
+ const file = path.join(root, 'package.json');
130
+ if (!fs.existsSync(file)) return null;
131
+ return readJson(file);
132
+ }
133
+
134
+ function hasCheckArchitectureScript(root) {
135
+ const pkg = readPackageJson(root);
136
+ return Boolean(pkg?.scripts?.['check:architecture']);
137
+ }
138
+
139
+ const REQUIRED_GATE_FILES = [
140
+ 'AGENTS.md',
141
+ '.mcp.json',
142
+ '.github/workflows/ark-check.yml',
143
+ ];
144
+
145
+ function missingGates(root) {
146
+ return REQUIRED_GATE_FILES.filter(
147
+ (relativePath) => !fs.existsSync(path.join(root, relativePath))
148
+ );
149
+ }
150
+
151
+ function checkArchitectureScriptSnippet() {
152
+ // npx resolves the installed package binary; `node bin/ark-check.mjs` only works
153
+ // inside Ark's own repo.
154
+ return '"check:architecture": "npx ark-check --root . --config ark.config.json --strict-config"';
155
+ }
156
+
98
157
  function readConfig(root, configPath) {
99
158
  const fullPath = path.isAbsolute(configPath)
100
159
  ? configPath
@@ -176,39 +235,71 @@ function runInit(args) {
176
235
  }
177
236
 
178
237
  const { srcDir, config } = detectConfig(args.root);
179
- if (config.layers.length === 0) {
180
- console.error(
181
- [
182
- 'No conventional layer directories found (looked for src/domain, src/application,',
183
- 'src/adapters/persistence, ...). Generate the full template instead and adapt the',
184
- 'patterns to your layout:',
185
- ' ark-check --print-config eleven-layer > ark.config.json',
186
- ].join('\n')
187
- );
188
- process.exitCode = 1;
189
- return;
190
- }
238
+ const greenfield = config.layers.length === 0;
239
+ // Greenfield: anchor the starter profile at src/ (the convention a fresh project will
240
+ // scaffold under) even when src/ doesn't exist yet — the layers are optional, so the
241
+ // check passes today and governance switches on the moment src/domain/ etc. appear.
242
+ const finalConfig = greenfield
243
+ ? createElevenLayerConfig({ rootDir: srcDir === '.' ? 'src' : srcDir })
244
+ : config;
191
245
 
192
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
246
+ fs.writeFileSync(configPath, `${JSON.stringify(finalConfig, null, 2)}\n`);
193
247
 
194
248
  console.log(`Wrote ${configPath}`);
195
249
  console.log('');
196
- console.log('Detected layers:');
197
- for (const layer of config.layers) {
198
- console.log(` ${layer.name}: ${layer.patterns.join(', ')}`);
199
- }
200
- const uncovered = uncoveredDirectories(args.root, srcDir, config.layers);
201
- if (uncovered.length > 0) {
202
- console.log('');
203
- console.log(
204
- `Not covered by any layer (add patterns for these or they stay ungoverned): ${uncovered.join(', ')}`
205
- );
250
+ if (greenfield) {
251
+ console.log('No conventional layer directories found — generated the full 11-layer starter');
252
+ console.log('profile instead. Every layer is marked optional, so the strict check passes now');
253
+ console.log('and each layer starts being enforced as soon as its directory gains source files:');
254
+ for (const layer of finalConfig.layers) {
255
+ console.log(` ${layer.name}: ${layer.patterns.join(', ')}`);
256
+ }
257
+ // The starter profile only governs src/. Existing source elsewhere would make the
258
+ // gate silently green, so surface it instead of pretending the project is covered.
259
+ const outside = walk(args.root)
260
+ .map((file) => normalize(path.relative(args.root, file)))
261
+ .filter((rel) => !rel.startsWith('src/') && !rel.split('/').some((s) => s.startsWith('.')));
262
+ if (outside.length > 0) {
263
+ console.log('');
264
+ console.log(`WARNING: ${outside.length} source file(s) live outside src/ and are NOT governed`);
265
+ console.log(`by this config (e.g. ${outside.slice(0, 3).join(', ')}).`);
266
+ console.log('Move them under src/, or edit the "include" and layer patterns to match your layout.');
267
+ }
268
+ } else {
269
+ console.log('Detected layers:');
270
+ for (const layer of finalConfig.layers) {
271
+ console.log(` ${layer.name}: ${layer.patterns.join(', ')}`);
272
+ }
273
+ const detected = new Set(finalConfig.layers.map((layer) => layer.name));
274
+ const suggested = DEFAULT_INTENT_PREFIXES.filter((entry) => !detected.has(entry.layer));
275
+ if (suggested.length > 0) {
276
+ console.log('');
277
+ console.log('Suggested layers from the 11-layer profile (not detected — conventional');
278
+ console.log('directories shown; create one and re-run --init, or add the layer by hand):');
279
+ for (const entry of suggested) {
280
+ const dirs = (DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? [])
281
+ .map((directory) => `${srcDir}/${directory}`)
282
+ .join(', ');
283
+ console.log(` ${entry.layer}: ${dirs}`);
284
+ }
285
+ }
286
+ const uncovered = uncoveredDirectories(args.root, srcDir, finalConfig.layers);
287
+ if (uncovered.length > 0) {
288
+ console.log('');
289
+ console.log(
290
+ `Not covered by any layer (add patterns for these or they stay ungoverned): ${uncovered.join(', ')}`
291
+ );
292
+ }
206
293
  }
207
294
  console.log('');
208
295
  console.log('Next steps:');
209
296
  console.log(' 1. CI gate: npx ark-check --root . --config ark.config.json --strict-config');
210
297
  console.log(' 2. AI write gate: npx ark-mcp --root . --config ark.config.json');
211
298
  console.log(' (bind its validate_code tool to your agent\'s pre-write hook — see README)');
299
+ if (!hasCheckArchitectureScript(args.root)) {
300
+ console.log(' 3. Add the npm alias if you want `npm run check:architecture`:');
301
+ console.log(` ${checkArchitectureScriptSnippet()}`);
302
+ }
212
303
  }
213
304
 
214
305
  function ensureDirForFile(file) {
@@ -220,9 +311,13 @@ function writeTemplate(root, relativePath, content, force) {
220
311
  if (fs.existsSync(fullPath) && !force) {
221
312
  return { relativePath, status: 'skipped' };
222
313
  }
223
- ensureDirForFile(fullPath);
224
- fs.writeFileSync(fullPath, content);
225
- return { relativePath, status: fs.existsSync(fullPath) ? 'written' : 'written' };
314
+ try {
315
+ ensureDirForFile(fullPath);
316
+ fs.writeFileSync(fullPath, content);
317
+ return { relativePath, status: 'written' };
318
+ } catch {
319
+ return { relativePath, status: 'failed' };
320
+ }
226
321
  }
227
322
 
228
323
  function packageManager(root) {
@@ -231,7 +326,7 @@ function packageManager(root) {
231
326
  cache: 'pnpm',
232
327
  setup: ['corepack enable'],
233
328
  install: 'pnpm install --frozen-lockfile',
234
- run: 'pnpm exec ark-check --root . --config ark.config.json --strict-config',
329
+ run: 'pnpm exec ark-check --root . --config ark.config.json --strict-config --require-gates',
235
330
  };
236
331
  }
237
332
  if (fs.existsSync(path.join(root, 'yarn.lock'))) {
@@ -239,27 +334,63 @@ function packageManager(root) {
239
334
  cache: 'yarn',
240
335
  setup: ['corepack enable'],
241
336
  install: 'yarn install --frozen-lockfile',
242
- run: 'yarn ark-check --root . --config ark.config.json --strict-config',
337
+ run: 'yarn ark-check --root . --config ark.config.json --strict-config --require-gates',
243
338
  };
244
339
  }
245
340
  return {
246
341
  cache: 'npm',
247
342
  setup: [],
248
343
  install: fs.existsSync(path.join(root, 'package-lock.json')) ? 'npm ci' : 'npm install',
249
- run: 'npx ark-check --root . --config ark.config.json --strict-config',
344
+ run: 'npx ark-check --root . --config ark.config.json --strict-config --require-gates',
250
345
  };
251
346
  }
252
347
 
348
+ const ARK_CHECK_COMMAND = 'npx ark-check --root . --config ark.config.json --strict-config';
349
+
350
+ // Canonical agent contract. AGENTS.md and the Cursor rule both derive from this
351
+ // single source so the steps can never drift out of sync between the two files.
352
+ const AGENT_CONTRACT = {
353
+ manifestResource: 'ark://manifest',
354
+ steps: [
355
+ `Read the Ark contract from \`ark://manifest\` when the MCP server is available.`,
356
+ `Keep source files inside the layer boundaries declared in \`ark.config.json\`.`,
357
+ `Do not bypass Ark publishers, event contracts, or source metadata for runtime mutations.`,
358
+ `After edits, run \`${ARK_CHECK_COMMAND}\`.`,
359
+ `If Ark reports violations, fix the architecture instead of weakening the gate.`,
360
+ ],
361
+ // Cursor-only guidance: the write-time validate_code tool is available in
362
+ // Cursor's runtime but has no equivalent in a plain AGENTS.md read.
363
+ cursorValidateStep: `Validate the full post-edit file content with the \`validate_code\` tool before writing whenever your runtime supports it.`,
364
+ };
365
+
366
+ function layerPlacementTable() {
367
+ const rows = DEFAULT_INTENT_PREFIXES.map((entry) => {
368
+ const dirs = (DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? [])
369
+ .map((directory) => `\`${directory}/\``)
370
+ .join(', ');
371
+ return `| ${entry.layer} | ${dirs} | ${entry.prefixes.map((p) => `\`${p}\``).join(', ')} |`;
372
+ }).join('\n');
373
+ return `| Layer | Conventional directories (under the source root) | Intent prefixes |
374
+ |-------|---------------------------------------------------|-----------------|
375
+ ${rows}`;
376
+ }
377
+
253
378
  function agentInstructions() {
379
+ const steps = AGENT_CONTRACT.steps.map((step, index) => `${index + 1}. ${step}`).join('\n');
254
380
  return `# Ark Enforcement
255
381
 
256
382
  Before editing TypeScript or JavaScript source files:
257
383
 
258
- 1. Read the Ark contract from \`ark://manifest\` when the MCP server is available.
259
- 2. Keep source files inside the layer boundaries declared in \`ark.config.json\`.
260
- 3. Do not bypass Ark publishers, event contracts, or source metadata for runtime mutations.
261
- 4. After edits, run \`npx ark-check --root . --config ark.config.json --strict-config\`.
262
- 5. If Ark reports violations, fix the architecture instead of weakening the gate.
384
+ ${steps}
385
+
386
+ ## Where new code belongs
387
+
388
+ \`ark.config.json\` is authoritative for this project. When creating a NEW kind of code
389
+ that no existing layer covers (a saga, a background job, a read model, ...), use the
390
+ default 11-layer placement below and add the layer to \`ark.config.json\` — do not invent
391
+ an ungoverned location:
392
+
393
+ ${layerPlacementTable()}
263
394
 
264
395
  The project is only considered Ark-enforced when the write gate, CI gate, and runtime path all pass.
265
396
  `;
@@ -291,13 +422,12 @@ alwaysApply: true
291
422
  ---
292
423
 
293
424
  Before writing or editing TypeScript or JavaScript source files, read the
294
- \`ark://manifest\` resource from the \`ark\` MCP server when available.
425
+ \`${AGENT_CONTRACT.manifestResource}\` resource from the \`ark\` MCP server when available.
295
426
 
296
- Validate the full post-edit file content with the \`validate_code\` tool before
297
- writing whenever your runtime supports it. After edits, run:
427
+ ${AGENT_CONTRACT.cursorValidateStep} After edits, run:
298
428
 
299
429
  \`\`\`bash
300
- npx ark-check --root . --config ark.config.json --strict-config
430
+ ${ARK_CHECK_COMMAND}
301
431
  \`\`\`
302
432
 
303
433
  If Ark reports violations, fix the architecture instead of bypassing the gate.
@@ -346,18 +476,59 @@ function claudeSettings() {
346
476
  }, null, 2)}\n`;
347
477
  }
348
478
 
479
+ function resolveTools(args) {
480
+ if (args.tools && args.tools.length > 0) {
481
+ return new Set(args.tools);
482
+ }
483
+ const root = args.root;
484
+ const detected = new Set();
485
+ if (fs.existsSync(path.join(root, '.claude'))) detected.add('claude');
486
+ if (fs.existsSync(path.join(root, '.cursor'))) detected.add('cursor');
487
+ if (fs.existsSync(path.join(root, '.codex'))) {
488
+ detected.add('codex');
489
+ }
490
+ // No signal at all: fall back to writing every tool's templates so a fresh
491
+ // project still gets a complete, reviewable starter set.
492
+ if (detected.size === 0) {
493
+ return new Set(['claude', 'cursor', 'codex']);
494
+ }
495
+ return detected;
496
+ }
497
+
498
+ const KNOWN_TOOLS = ['claude', 'cursor', 'codex'];
499
+
349
500
  function runInstallAgentGates(args) {
350
501
  const root = args.root;
502
+ if (args.tools) {
503
+ const unknown = args.tools.filter((tool) => !KNOWN_TOOLS.includes(tool));
504
+ if (args.tools.length === 0 || unknown.length > 0) {
505
+ console.error(
506
+ `--tools expects a comma-separated subset of: ${KNOWN_TOOLS.join(', ')}` +
507
+ (unknown.length > 0 ? ` (unknown: ${unknown.join(', ')})` : '')
508
+ );
509
+ process.exitCode = 2;
510
+ return;
511
+ }
512
+ }
351
513
  const pm = packageManager(root);
514
+ const hasCheckScript = hasCheckArchitectureScript(root);
515
+ const tools = resolveTools(args);
352
516
  const templates = [
517
+ // Base gates: tool-agnostic contract + CI backstop, always written.
353
518
  ['AGENTS.md', agentInstructions()],
354
519
  ['.mcp.json', mcpJson()],
355
- ['.cursor/mcp.json', mcpJson()],
356
- ['.cursor/rules/ark.mdc', cursorRule()],
357
- ['.claude/settings.json', claudeSettings()],
358
520
  ['.github/workflows/ark-check.yml', githubWorkflow(pm)],
359
- ['docs/ark-codex-config.toml', codexTomlSnippet()],
360
521
  ];
522
+ if (tools.has('cursor')) {
523
+ templates.push(['.cursor/mcp.json', mcpJson()]);
524
+ templates.push(['.cursor/rules/ark.mdc', cursorRule()]);
525
+ }
526
+ if (tools.has('claude')) {
527
+ templates.push(['.claude/settings.json', claudeSettings()]);
528
+ }
529
+ if (tools.has('codex')) {
530
+ templates.push(['docs/ark-codex-config.toml', codexTomlSnippet()]);
531
+ }
361
532
 
362
533
  const results = templates.map(([relativePath, content]) =>
363
534
  writeTemplate(root, relativePath, content, args.force)
@@ -365,14 +536,27 @@ function runInstallAgentGates(args) {
365
536
 
366
537
  console.log('Ark agent gate templates:');
367
538
  for (const result of results) {
368
- const marker = result.status === 'written' ? 'wrote' : 'skipped';
539
+ const marker =
540
+ result.status === 'written' ? 'wrote' : result.status === 'failed' ? 'FAILED' : 'skipped';
369
541
  console.log(` ${marker.padEnd(7)} ${result.relativePath}`);
370
542
  }
543
+ const failed = results.filter((result) => result.status === 'failed');
544
+ if (failed.length > 0) {
545
+ console.error(`\nFailed to write ${failed.length} template(s).`);
546
+ process.exitCode = 1;
547
+ return;
548
+ }
371
549
  console.log('');
372
550
  console.log('Next steps:');
373
551
  console.log(' 1. Review the generated files and commit the ones that match your tools.');
374
552
  console.log(' 2. Run: npx ark-check --root . --config ark.config.json --strict-config');
375
- console.log(' 3. Wire Codex manually from docs/ark-codex-config.toml if your host uses ~/.codex/config.toml.');
553
+ if (!hasCheckScript) {
554
+ console.log(' 3. Add the npm alias if you want `npm run check:architecture`:');
555
+ console.log(` ${checkArchitectureScriptSnippet()}`);
556
+ console.log(' 4. If you use Codex in this project, wire it now so `ark://manifest` is available from the first edit.');
557
+ } else {
558
+ console.log(' 3. If you use Codex in this project, wire it now so `ark://manifest` is available from the first edit.');
559
+ }
376
560
  }
377
561
 
378
562
  function readManifest(root, manifestPath) {
@@ -854,6 +1038,35 @@ async function main() {
854
1038
  return;
855
1039
  }
856
1040
 
1041
+ if (args.requireGates) {
1042
+ const missing = missingGates(args.root);
1043
+ if (missing.length > 0) {
1044
+ const payload = {
1045
+ ok: false,
1046
+ error: 'missing-gates',
1047
+ missing,
1048
+ };
1049
+ if (args.json) {
1050
+ console.log(JSON.stringify(payload, null, 2));
1051
+ } else {
1052
+ console.error('Ark gates are not installed. Missing:');
1053
+ for (const relativePath of missing) {
1054
+ console.error(` - ${relativePath}`);
1055
+ }
1056
+ console.error('\nRun `npx ark init` (or `ark-check --install-agent-gates`) to configure enforcement.');
1057
+ }
1058
+ process.exitCode = 1;
1059
+ return;
1060
+ }
1061
+ // Gates present. This is a precondition, not a standalone report: stay quiet
1062
+ // in --json mode so the architecture check below owns the single JSON output.
1063
+ // When --require-gates is the only intent (no config/architecture run needed),
1064
+ // callers still get a clear signal from the exit code and the human-mode line.
1065
+ if (!args.json) {
1066
+ console.log('Ark gates present: ' + REQUIRED_GATE_FILES.join(', '));
1067
+ }
1068
+ }
1069
+
857
1070
  let ts;
858
1071
  try {
859
1072
  ts = await import('typescript');
package/bin/ark-mcp.mjs CHANGED
@@ -25,7 +25,12 @@
25
25
  import fs from 'node:fs';
26
26
  import path from 'node:path';
27
27
  import readline from 'node:readline';
28
- import { DEFAULT_INTENT_PREFIXES, DEFAULT_RULES, layerForFile } from './ark-shared.mjs';
28
+ import {
29
+ DEFAULT_INTENT_PREFIXES,
30
+ DEFAULT_LAYER_DIRECTORIES,
31
+ DEFAULT_RULES,
32
+ layerForFile,
33
+ } from './ark-shared.mjs';
29
34
 
30
35
  function parseArgs(argv) {
31
36
  const args = {
@@ -297,6 +302,31 @@ async function main() {
297
302
  },
298
303
  ];
299
304
 
305
+ // Layers from the 11-layer profile that this project has NOT declared, with their
306
+ // conventional directories: tells the agent where a new kind of code (a saga, a job,
307
+ // a read model, ...) belongs BEFORE it improvises a location the gate can't govern.
308
+ // A default layer is dropped when the project already claims any of its intent
309
+ // prefixes under another name (e.g. a `core` layer owning `Domain.`) — suggesting
310
+ // DomainModel there would tell the agent to create a second layer for the same
311
+ // prefix, making longest-prefix resolution ambiguous.
312
+ function suggestedLayers() {
313
+ const activeNames = new Set(profile.layers.map((layer) => layer.name));
314
+ const claimedPrefixes = new Set(
315
+ profile.layers.flatMap((layer) =>
316
+ (layer.prefixes ?? []).map((p) => (p.endsWith('.') ? p : `${p}.`))
317
+ )
318
+ );
319
+ return DEFAULT_INTENT_PREFIXES.filter(
320
+ (entry) =>
321
+ !activeNames.has(entry.layer) &&
322
+ !entry.prefixes.some((p) => claimedPrefixes.has(p.endsWith('.') ? p : `${p}.`))
323
+ ).map((entry) => ({
324
+ layer: entry.layer,
325
+ intentPrefixes: entry.prefixes,
326
+ conventionalDirectories: DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? [],
327
+ }));
328
+ }
329
+
300
330
  function manifestText() {
301
331
  if (projectManifest) {
302
332
  return JSON.stringify(
@@ -305,12 +335,23 @@ async function main() {
305
335
  2
306
336
  );
307
337
  }
338
+ const suggestions = suggestedLayers();
308
339
  return JSON.stringify(
309
340
  {
310
341
  source: profile === ark.elevenLayerProfile ? 'strictDefaultElevenLayerProfile' : 'project',
311
342
  name: profile.name,
312
343
  layers: profile.layers,
313
344
  rules: profile.rules,
345
+ ...(suggestions.length > 0
346
+ ? {
347
+ suggestedLayers: suggestions,
348
+ suggestedLayersNote:
349
+ 'Layers from the default 11-layer profile this project has not declared. ' +
350
+ 'When creating a NEW kind of code that fits one of these, place it in a ' +
351
+ 'conventional directory and add the layer to ark.config.json instead of ' +
352
+ 'inventing an ungoverned location.',
353
+ }
354
+ : {}),
314
355
  },
315
356
  null,
316
357
  2
@@ -72,12 +72,13 @@ export const DEFAULT_RULES = createStrictDenyRules(
72
72
  export function createElevenLayerConfig(options = {}) {
73
73
  const rootDir = options.rootDir ?? 'src';
74
74
  const optional = options.optionalLayers ?? true;
75
+ const prefix = rootDir === '.' ? '' : `${rootDir}/`;
75
76
  return {
76
77
  include: options.include ?? [rootDir],
77
78
  layers: DEFAULT_INTENT_PREFIXES.map((entry) => ({
78
79
  name: entry.layer,
79
80
  patterns: (DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? [entry.layer]).map(
80
- (directory) => `${rootDir}/${directory}/**`
81
+ (directory) => `${prefix}${directory}/**`
81
82
  ),
82
83
  intentPrefixes: entry.prefixes,
83
84
  optional,
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  // src/version.ts
4
- var version = "1.2.0";
4
+ var version = "1.3.0";
5
5
 
6
6
  // src/kernel/intent/IntentRegistry.ts
7
7
  var IntentRegistry = class {
@@ -1833,12 +1833,13 @@ var defaultElevenLayerDirectories = {
1833
1833
  function createElevenLayerArkConfig(options = {}) {
1834
1834
  const rootDir = options.rootDir ?? "src";
1835
1835
  const optional = options.optionalLayers ?? true;
1836
+ const prefix = rootDir === "." ? "" : `${rootDir}/`;
1836
1837
  return {
1837
1838
  include: options.include ?? [rootDir],
1838
1839
  layers: elevenLayerProfile.layers.map((layer) => ({
1839
1840
  name: layer.name,
1840
1841
  patterns: (defaultElevenLayerDirectories[layer.name] ?? [layer.name]).map(
1841
- (directory) => `${rootDir}/${directory}/**`
1842
+ (directory) => `${prefix}${directory}/**`
1842
1843
  ),
1843
1844
  intentPrefixes: layer.prefixes,
1844
1845
  optional