ark-runtime-kernel 1.1.0 → 1.2.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 +14 -1
- package/bin/ark-check.mjs +174 -0
- package/bin/ark-postinstall.mjs +12 -0
- package/bin/ark.mjs +129 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/nestjs/index.js.map +1 -1
- package/docs/agent-guide.md +344 -0
- package/docs/ai-gates.md +169 -0
- package/docs/ark-check-example.json +87 -0
- package/docs/assets/ark-write-gate.svg +28 -0
- package/docs/blog/how-i-stopped-claude-from-breaking-my-architecture.md +85 -0
- package/docs/production-hardening.md +59 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ No code changes. No new runtime. Just a config and a CI line.
|
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
npm install -D ark-runtime-kernel typescript
|
|
33
|
-
npx ark
|
|
33
|
+
npx ark init # asks before generating config, agent gates, and CI templates
|
|
34
34
|
npx ark-check # done: cross-layer imports now fail the check
|
|
35
35
|
```
|
|
36
36
|
|
|
@@ -58,6 +58,19 @@ Then gate your agents (Claude Code shown; [Cursor / Codex / others](docs/ai-gate
|
|
|
58
58
|
|
|
59
59
|
> The same `ark.config.json` powers every gate.
|
|
60
60
|
|
|
61
|
+
Or generate the starter agent and CI gate files:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx ark-check --install-agent-gates
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This writes opt-in templates for MCP discovery, Claude/Cursor rules, Codex config notes,
|
|
68
|
+
GitHub Actions, and agent instructions. Existing files are skipped unless you pass
|
|
69
|
+
`--force`.
|
|
70
|
+
|
|
71
|
+
The package `postinstall` only prints the next command; it never prompts or writes files
|
|
72
|
+
during `npm install`. Use `npx ark init --yes` for non-interactive setup.
|
|
73
|
+
|
|
61
74
|
## Why Ark (and not just a linter)?
|
|
62
75
|
|
|
63
76
|
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
|
@@ -22,6 +22,7 @@ function parseArgs(argv) {
|
|
|
22
22
|
json: false,
|
|
23
23
|
strictConfig: false,
|
|
24
24
|
init: false,
|
|
25
|
+
installAgentGates: false,
|
|
25
26
|
force: false,
|
|
26
27
|
baseline: undefined,
|
|
27
28
|
updateBaseline: false,
|
|
@@ -31,6 +32,7 @@ function parseArgs(argv) {
|
|
|
31
32
|
if (arg === '--json') args.json = true;
|
|
32
33
|
else if (arg === '--strict-config') args.strictConfig = true;
|
|
33
34
|
else if (arg === '--init') args.init = true;
|
|
35
|
+
else if (arg === '--install-agent-gates') args.installAgentGates = true;
|
|
34
36
|
else if (arg === '--force') args.force = true;
|
|
35
37
|
else if (arg === '--baseline' || arg === '--update-baseline') {
|
|
36
38
|
if (arg === '--update-baseline') args.updateBaseline = true;
|
|
@@ -52,6 +54,7 @@ function usage() {
|
|
|
52
54
|
return [
|
|
53
55
|
'Usage: ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-config] [--json] [--baseline [file]]',
|
|
54
56
|
' ark-check --init [--force]',
|
|
57
|
+
' ark-check --install-agent-gates [--force]',
|
|
55
58
|
' ark-check --update-baseline [file] freeze current violations (default .ark-baseline.json)',
|
|
56
59
|
' ark-check --print-config eleven-layer',
|
|
57
60
|
'',
|
|
@@ -82,6 +85,9 @@ function usage() {
|
|
|
82
85
|
'',
|
|
83
86
|
'Generate a starter 11-layer config:',
|
|
84
87
|
' ark-check --print-config eleven-layer > ark.config.json',
|
|
88
|
+
'',
|
|
89
|
+
'Install agent + CI enforcement templates:',
|
|
90
|
+
' ark-check --install-agent-gates',
|
|
85
91
|
].join('\n');
|
|
86
92
|
}
|
|
87
93
|
|
|
@@ -205,6 +211,170 @@ function runInit(args) {
|
|
|
205
211
|
console.log(' (bind its validate_code tool to your agent\'s pre-write hook — see README)');
|
|
206
212
|
}
|
|
207
213
|
|
|
214
|
+
function ensureDirForFile(file) {
|
|
215
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function writeTemplate(root, relativePath, content, force) {
|
|
219
|
+
const fullPath = path.join(root, relativePath);
|
|
220
|
+
if (fs.existsSync(fullPath) && !force) {
|
|
221
|
+
return { relativePath, status: 'skipped' };
|
|
222
|
+
}
|
|
223
|
+
ensureDirForFile(fullPath);
|
|
224
|
+
fs.writeFileSync(fullPath, content);
|
|
225
|
+
return { relativePath, status: fs.existsSync(fullPath) ? 'written' : 'written' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function packageManager(root) {
|
|
229
|
+
if (fs.existsSync(path.join(root, 'pnpm-lock.yaml'))) {
|
|
230
|
+
return {
|
|
231
|
+
cache: 'pnpm',
|
|
232
|
+
setup: ['corepack enable'],
|
|
233
|
+
install: 'pnpm install --frozen-lockfile',
|
|
234
|
+
run: 'pnpm exec ark-check --root . --config ark.config.json --strict-config',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (fs.existsSync(path.join(root, 'yarn.lock'))) {
|
|
238
|
+
return {
|
|
239
|
+
cache: 'yarn',
|
|
240
|
+
setup: ['corepack enable'],
|
|
241
|
+
install: 'yarn install --frozen-lockfile',
|
|
242
|
+
run: 'yarn ark-check --root . --config ark.config.json --strict-config',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
cache: 'npm',
|
|
247
|
+
setup: [],
|
|
248
|
+
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',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function agentInstructions() {
|
|
254
|
+
return `# Ark Enforcement
|
|
255
|
+
|
|
256
|
+
Before editing TypeScript or JavaScript source files:
|
|
257
|
+
|
|
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.
|
|
263
|
+
|
|
264
|
+
The project is only considered Ark-enforced when the write gate, CI gate, and runtime path all pass.
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function mcpJson() {
|
|
269
|
+
return `${JSON.stringify({
|
|
270
|
+
mcpServers: {
|
|
271
|
+
ark: {
|
|
272
|
+
type: 'stdio',
|
|
273
|
+
command: 'npx',
|
|
274
|
+
args: ['ark-mcp', '--root', '.', '--config', 'ark.config.json'],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
}, null, 2)}\n`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function codexTomlSnippet() {
|
|
281
|
+
return `[mcp_servers.ark]
|
|
282
|
+
command = "npx"
|
|
283
|
+
args = ["ark-mcp", "--root", ".", "--config", "ark.config.json"]
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function cursorRule() {
|
|
288
|
+
return `---
|
|
289
|
+
description: Ark architecture contract
|
|
290
|
+
alwaysApply: true
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
Before writing or editing TypeScript or JavaScript source files, read the
|
|
294
|
+
\`ark://manifest\` resource from the \`ark\` MCP server when available.
|
|
295
|
+
|
|
296
|
+
Validate the full post-edit file content with the \`validate_code\` tool before
|
|
297
|
+
writing whenever your runtime supports it. After edits, run:
|
|
298
|
+
|
|
299
|
+
\`\`\`bash
|
|
300
|
+
npx ark-check --root . --config ark.config.json --strict-config
|
|
301
|
+
\`\`\`
|
|
302
|
+
|
|
303
|
+
If Ark reports violations, fix the architecture instead of bypassing the gate.
|
|
304
|
+
`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function githubWorkflow(pm) {
|
|
308
|
+
const setupSteps = pm.setup.map((command) => ` - run: ${command}`).join('\n');
|
|
309
|
+
return `name: Ark architecture gate
|
|
310
|
+
|
|
311
|
+
on:
|
|
312
|
+
pull_request:
|
|
313
|
+
push:
|
|
314
|
+
branches: [main, master]
|
|
315
|
+
|
|
316
|
+
jobs:
|
|
317
|
+
ark-check:
|
|
318
|
+
runs-on: ubuntu-latest
|
|
319
|
+
steps:
|
|
320
|
+
- uses: actions/checkout@v4
|
|
321
|
+
- uses: actions/setup-node@v4
|
|
322
|
+
with:
|
|
323
|
+
node-version: 20
|
|
324
|
+
cache: ${pm.cache}
|
|
325
|
+
${setupSteps ? `${setupSteps}\n` : ''} - run: ${pm.install}
|
|
326
|
+
- run: ${pm.run}
|
|
327
|
+
`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function claudeSettings() {
|
|
331
|
+
return `${JSON.stringify({
|
|
332
|
+
hooks: {
|
|
333
|
+
PreToolUse: [
|
|
334
|
+
{
|
|
335
|
+
matcher: 'Write|Edit|MultiEdit',
|
|
336
|
+
hooks: [
|
|
337
|
+
{
|
|
338
|
+
type: 'command',
|
|
339
|
+
command:
|
|
340
|
+
'npx ark-mcp --hook --root "$CLAUDE_PROJECT_DIR" --config ark.config.json',
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
}, null, 2)}\n`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function runInstallAgentGates(args) {
|
|
350
|
+
const root = args.root;
|
|
351
|
+
const pm = packageManager(root);
|
|
352
|
+
const templates = [
|
|
353
|
+
['AGENTS.md', agentInstructions()],
|
|
354
|
+
['.mcp.json', mcpJson()],
|
|
355
|
+
['.cursor/mcp.json', mcpJson()],
|
|
356
|
+
['.cursor/rules/ark.mdc', cursorRule()],
|
|
357
|
+
['.claude/settings.json', claudeSettings()],
|
|
358
|
+
['.github/workflows/ark-check.yml', githubWorkflow(pm)],
|
|
359
|
+
['docs/ark-codex-config.toml', codexTomlSnippet()],
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
const results = templates.map(([relativePath, content]) =>
|
|
363
|
+
writeTemplate(root, relativePath, content, args.force)
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
console.log('Ark agent gate templates:');
|
|
367
|
+
for (const result of results) {
|
|
368
|
+
const marker = result.status === 'written' ? 'wrote' : 'skipped';
|
|
369
|
+
console.log(` ${marker.padEnd(7)} ${result.relativePath}`);
|
|
370
|
+
}
|
|
371
|
+
console.log('');
|
|
372
|
+
console.log('Next steps:');
|
|
373
|
+
console.log(' 1. Review the generated files and commit the ones that match your tools.');
|
|
374
|
+
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.');
|
|
376
|
+
}
|
|
377
|
+
|
|
208
378
|
function readManifest(root, manifestPath) {
|
|
209
379
|
if (!manifestPath) return undefined;
|
|
210
380
|
const fullPath = path.isAbsolute(manifestPath)
|
|
@@ -670,6 +840,10 @@ async function main() {
|
|
|
670
840
|
runInit(args);
|
|
671
841
|
return;
|
|
672
842
|
}
|
|
843
|
+
if (args.installAgentGates) {
|
|
844
|
+
runInstallAgentGates(args);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
673
847
|
if (args.printConfig) {
|
|
674
848
|
if (args.printConfig !== 'eleven-layer') {
|
|
675
849
|
console.error(`Unknown config profile: ${args.printConfig}`);
|
package/bin/ark.mjs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import readline from 'node:readline/promises';
|
|
7
|
+
|
|
8
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const arkCheck = path.join(here, 'ark-check.mjs');
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const args = {
|
|
13
|
+
command: argv[2],
|
|
14
|
+
root: process.cwd(),
|
|
15
|
+
yes: false,
|
|
16
|
+
force: false,
|
|
17
|
+
strict: true,
|
|
18
|
+
help: false,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (let i = 3; i < argv.length; i += 1) {
|
|
22
|
+
const arg = argv[i];
|
|
23
|
+
if (arg === '--root') args.root = path.resolve(argv[++i]);
|
|
24
|
+
else if (arg === '--yes' || arg === '-y') args.yes = true;
|
|
25
|
+
else if (arg === '--force') args.force = true;
|
|
26
|
+
else if (arg === '--no-strict') args.strict = false;
|
|
27
|
+
else if (arg === '--help' || arg === '-h') args.help = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return args;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function usage() {
|
|
34
|
+
return `Usage:
|
|
35
|
+
ark init [--root <project>] [--yes] [--force] [--no-strict]
|
|
36
|
+
|
|
37
|
+
Commands:
|
|
38
|
+
init Configure Ark project enforcement with explicit prompts.
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--yes Non-interactive defaults: create config if needed, install gate templates, run strict check.
|
|
42
|
+
--force Allow generated files to overwrite existing files.
|
|
43
|
+
--no-strict Skip the final strict ark-check run.
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function runArkCheck(args, options = {}) {
|
|
48
|
+
const result = spawnSync(process.execPath, [arkCheck, ...args], {
|
|
49
|
+
cwd: options.cwd,
|
|
50
|
+
stdio: options.stdio ?? 'inherit',
|
|
51
|
+
encoding: 'utf8',
|
|
52
|
+
});
|
|
53
|
+
return result.status ?? 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function askYesNo(rl, question, defaultYes = true) {
|
|
57
|
+
const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
|
|
58
|
+
const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
|
|
59
|
+
if (!answer) return defaultYes;
|
|
60
|
+
return answer === 'y' || answer === 'yes';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function init(args) {
|
|
64
|
+
const root = args.root;
|
|
65
|
+
const configPath = path.join(root, 'ark.config.json');
|
|
66
|
+
const rl = args.yes
|
|
67
|
+
? null
|
|
68
|
+
: readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
let shouldInit = !fs.existsSync(configPath);
|
|
72
|
+
if (fs.existsSync(configPath)) {
|
|
73
|
+
shouldInit = args.force
|
|
74
|
+
? true
|
|
75
|
+
: args.yes
|
|
76
|
+
? false
|
|
77
|
+
: await askYesNo(rl, 'ark.config.json already exists. Regenerate it?', false);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (shouldInit) {
|
|
81
|
+
const initArgs = ['--root', root, '--init'];
|
|
82
|
+
if (args.force) initArgs.push('--force');
|
|
83
|
+
const status = runArkCheck(initArgs, { cwd: root });
|
|
84
|
+
if (status !== 0) return status;
|
|
85
|
+
} else {
|
|
86
|
+
console.log('Skipped ark.config.json generation.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const installGates = args.yes || await askYesNo(rl, 'Configure agent and CI gate templates?', true);
|
|
90
|
+
if (installGates) {
|
|
91
|
+
const gateArgs = ['--root', root, '--install-agent-gates'];
|
|
92
|
+
if (args.force) gateArgs.push('--force');
|
|
93
|
+
const status = runArkCheck(gateArgs, { cwd: root });
|
|
94
|
+
if (status !== 0) return status;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const runStrict =
|
|
98
|
+
args.strict && (args.yes || await askYesNo(rl, 'Run strict architecture check now?', true));
|
|
99
|
+
if (runStrict) {
|
|
100
|
+
return runArkCheck(
|
|
101
|
+
['--root', root, '--config', 'ark.config.json', '--strict-config'],
|
|
102
|
+
{ cwd: root }
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log('Ark init complete. Run `npx ark-check --root . --config ark.config.json --strict-config` before merging.');
|
|
107
|
+
return 0;
|
|
108
|
+
} finally {
|
|
109
|
+
rl?.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function main() {
|
|
114
|
+
const args = parseArgs(process.argv);
|
|
115
|
+
if (args.help || !args.command) {
|
|
116
|
+
console.log(usage());
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (args.command === 'init') {
|
|
121
|
+
return init(args);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.error(`Unknown command: ${args.command}`);
|
|
125
|
+
console.error(usage());
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
process.exitCode = await main();
|