arkgate 2.13.0 → 3.0.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/CHANGELOG.md +39 -0
- package/README.md +37 -22
- package/bin/ark-check.mjs +62 -4
- package/bin/ark-mcp.mjs +108 -1
- package/bin/ark-shared.mjs +204 -149
- package/bin/ark.mjs +90 -25
- package/bin/lib/adapter-contract.mjs +93 -0
- package/bin/lib/agent-gates.mjs +1 -0
- package/bin/lib/analysis-engine.mjs +1171 -0
- package/bin/lib/architecture-scan.mjs +84 -135
- package/bin/lib/ci-and-commands.mjs +31 -0
- package/bin/lib/config-warnings.mjs +7 -205
- package/bin/lib/field-install.mjs +67 -10
- package/bin/lib/gate-files.mjs +42 -3
- package/bin/lib/graph-cycles.mjs +4 -54
- package/bin/lib/hook-templates.mjs +33 -1
- package/bin/lib/host-support-matrix.mjs +7 -1
- package/bin/lib/install-migrate.mjs +54 -16
- package/bin/lib/presets.mjs +42 -2
- package/bin/lib/safety-diagnostics.mjs +18 -17
- package/bin/lib/scan-files.mjs +12 -1
- package/bin/lib/skill-install.mjs +8 -1
- package/bin/lib/source-policy.mjs +36 -0
- package/bin/lib/start-preview.mjs +271 -0
- package/bin/lib/ts-resolve.mjs +11 -2
- package/bin/lib/write-path-capabilities.mjs +4 -0
- package/compat/nestjs.cjs +2 -0
- package/compat/nestjs.d.ts +2 -0
- package/compat/nestjs.js +1 -0
- package/compat/runtime.cjs +2 -0
- package/compat/runtime.d.ts +2 -0
- package/compat/runtime.js +1 -0
- package/dist/configContract-BxSIwVRo.d.cts +259 -0
- package/dist/configContract-BxSIwVRo.d.ts +259 -0
- package/dist/eslint/index.cjs +125 -48
- package/dist/eslint/index.d.cts +7 -1
- package/dist/eslint/index.d.ts +7 -1
- package/dist/eslint/index.js +125 -48
- package/dist/index.cjs +1248 -3302
- package/dist/index.d.cts +359 -483
- package/dist/index.d.ts +359 -483
- package/dist/index.js +1231 -3248
- package/docs/agent-guide.md +28 -16
- package/docs/ai-gates.md +30 -7
- package/docs/migrate-from-ark-runtime-kernel.md +2 -3
- package/docs/package-surface.md +8 -13
- package/docs/production-hardening.md +17 -4
- package/docs/typescript-support.md +27 -0
- package/package.json +33 -11
- package/schemas/ark.analysis-result.schema.json +91 -0
- package/server.json +2 -2
- package/templates/skills/ark-architect.md +3 -2
- package/dist/configContract-iBLxx5Tz.d.cts +0 -53
- package/dist/configContract-iBLxx5Tz.d.ts +0 -53
- package/dist/eslint/index.cjs.map +0 -1
- package/dist/eslint/index.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/nestjs/index.cjs +0 -2606
- package/dist/nestjs/index.cjs.map +0 -1
- package/dist/nestjs/index.d.cts +0 -23
- package/dist/nestjs/index.d.ts +0 -23
- package/dist/nestjs/index.js +0 -2582
- package/dist/nestjs/index.js.map +0 -1
- package/dist/runtime/index.cjs +0 -4014
- package/dist/runtime/index.cjs.map +0 -1
- package/dist/runtime/index.d.cts +0 -3
- package/dist/runtime/index.d.ts +0 -3
- package/dist/runtime/index.js +0 -3925
- package/dist/runtime/index.js.map +0 -1
- package/dist/types-BxBwnBpC.d.cts +0 -1041
- package/dist/types-Wcs_l1_J.d.ts +0 -1041
|
@@ -21,6 +21,68 @@ function arkPackageVersion() {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function matchingBrace(text, openIndex) {
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let quoted = false;
|
|
27
|
+
let escaped = false;
|
|
28
|
+
for (let index = openIndex; index < text.length; index += 1) {
|
|
29
|
+
const char = text[index];
|
|
30
|
+
if (quoted) {
|
|
31
|
+
if (escaped) escaped = false;
|
|
32
|
+
else if (char === '\\') escaped = true;
|
|
33
|
+
else if (char === '"') quoted = false;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (char === '"') quoted = true;
|
|
37
|
+
else if (char === '{') depth += 1;
|
|
38
|
+
else if (char === '}' && --depth === 0) return index;
|
|
39
|
+
}
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function addDevDependencyPreservingFormat(source, version) {
|
|
44
|
+
const multiline = /\r?\n/.test(source);
|
|
45
|
+
const eol = source.includes('\r\n') ? '\r\n' : '\n';
|
|
46
|
+
const rootPropertyIndent = source.match(/\r?\n([ \t]+)"[^"\n]+"\s*:/)?.[1] ?? ' ';
|
|
47
|
+
const indentUnit = rootPropertyIndent;
|
|
48
|
+
const encoded = JSON.stringify(version);
|
|
49
|
+
const devMatch = /"devDependencies"\s*:\s*\{/.exec(source);
|
|
50
|
+
|
|
51
|
+
if (devMatch) {
|
|
52
|
+
const open = source.indexOf('{', devMatch.index);
|
|
53
|
+
const close = matchingBrace(source, open);
|
|
54
|
+
if (close === -1) throw new Error('Unbalanced devDependencies object');
|
|
55
|
+
const body = source.slice(open + 1, close);
|
|
56
|
+
if (!multiline) {
|
|
57
|
+
const addition = body.trim() ? `,"arkgate":${encoded}` : `"arkgate":${encoded}`;
|
|
58
|
+
return `${source.slice(0, close)}${addition}${source.slice(close)}`;
|
|
59
|
+
}
|
|
60
|
+
const beforeClose = source.slice(0, close);
|
|
61
|
+
const trailing = beforeClose.match(/\s*$/)?.[0] ?? '';
|
|
62
|
+
const contentEnd = close - trailing.length;
|
|
63
|
+
const closingIndent = trailing.slice(trailing.lastIndexOf('\n') + 1);
|
|
64
|
+
const propertyIndent = `${closingIndent}${indentUnit}`;
|
|
65
|
+
const addition = body.trim()
|
|
66
|
+
? `,${eol}${propertyIndent}"arkgate": ${encoded}`
|
|
67
|
+
: `${propertyIndent}"arkgate": ${encoded}`;
|
|
68
|
+
return `${source.slice(0, contentEnd)}${addition}${eol}${closingIndent}${source.slice(close)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rootClose = source.lastIndexOf('}');
|
|
72
|
+
if (rootClose === -1) throw new Error('Unbalanced package.json object');
|
|
73
|
+
const rootBody = source.slice(0, rootClose);
|
|
74
|
+
if (!multiline) {
|
|
75
|
+
const separator = rootBody.trim().endsWith('{') ? '' : ',';
|
|
76
|
+
return `${rootBody}${separator}"devDependencies":{"arkgate":${encoded}}${source.slice(rootClose)}`;
|
|
77
|
+
}
|
|
78
|
+
const trailing = rootBody.match(/\s*$/)?.[0] ?? '';
|
|
79
|
+
const contentEnd = rootClose - trailing.length;
|
|
80
|
+
const rootClosingIndent = trailing.slice(trailing.lastIndexOf('\n') + 1);
|
|
81
|
+
const separator = source.slice(0, contentEnd).trimEnd().endsWith('{') ? '' : ',';
|
|
82
|
+
const addition = `${separator}${eol}${rootPropertyIndent}"devDependencies": {${eol}${rootPropertyIndent}${indentUnit}"arkgate": ${encoded}${eol}${rootPropertyIndent}}`;
|
|
83
|
+
return `${source.slice(0, contentEnd)}${addition}${eol}${rootClosingIndent}${source.slice(rootClose)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
24
86
|
/**
|
|
25
87
|
* Ensure a check command string includes `--baseline <file>`.
|
|
26
88
|
* Only touches strings that already invoke ark-check / arkgate-check.
|
|
@@ -150,16 +212,15 @@ export function pinArkgateDevDependency(root, opts = {}) {
|
|
|
150
212
|
return { changed: false, reason: 'no-package-json' };
|
|
151
213
|
}
|
|
152
214
|
let pkg;
|
|
215
|
+
let source;
|
|
153
216
|
try {
|
|
154
|
-
|
|
217
|
+
source = fs.readFileSync(pkgPath, 'utf8');
|
|
218
|
+
pkg = JSON.parse(source);
|
|
155
219
|
} catch {
|
|
156
220
|
return { changed: false, reason: 'unreadable-package-json' };
|
|
157
221
|
}
|
|
158
222
|
const deps = pkg.dependencies && typeof pkg.dependencies === 'object' ? pkg.dependencies : {};
|
|
159
|
-
const dev =
|
|
160
|
-
pkg.devDependencies && typeof pkg.devDependencies === 'object'
|
|
161
|
-
? { ...pkg.devDependencies }
|
|
162
|
-
: {};
|
|
223
|
+
const dev = pkg.devDependencies && typeof pkg.devDependencies === 'object' ? pkg.devDependencies : {};
|
|
163
224
|
if (typeof deps.arkgate === 'string' || typeof dev.arkgate === 'string') {
|
|
164
225
|
return {
|
|
165
226
|
changed: false,
|
|
@@ -174,12 +235,8 @@ export function pinArkgateDevDependency(root, opts = {}) {
|
|
|
174
235
|
: shipped
|
|
175
236
|
? `^${shipped}`
|
|
176
237
|
: 'latest';
|
|
177
|
-
dev.arkgate = version;
|
|
178
238
|
if (opts.write !== false) {
|
|
179
|
-
fs.writeFileSync(
|
|
180
|
-
pkgPath,
|
|
181
|
-
`${JSON.stringify({ ...pkg, devDependencies: dev }, null, 2)}\n`
|
|
182
|
-
);
|
|
239
|
+
fs.writeFileSync(pkgPath, addDevDependencyPreservingFormat(source, version));
|
|
183
240
|
}
|
|
184
241
|
return { changed: true, reason: 'added', version };
|
|
185
242
|
}
|
package/bin/lib/gate-files.mjs
CHANGED
|
@@ -119,6 +119,40 @@ export const REQUIRED_GATE_FILES = [
|
|
|
119
119
|
'.mcp.json',
|
|
120
120
|
];
|
|
121
121
|
const REQUIRED_GATE_WORKFLOW = '.github/workflows/*.yml running ark-check';
|
|
122
|
+
const COMPACT_ROUTER = /<!--\s*arkgate:compact-router host=([a-z]+)\s*-->/;
|
|
123
|
+
|
|
124
|
+
const COMPACT_HOST_FILES = {
|
|
125
|
+
claude: ['.claude/settings.json'],
|
|
126
|
+
grok: ['.grok/config.toml', '.grok/hooks/ark-write-gate.json'],
|
|
127
|
+
cursor: ['.cursor/mcp.json'],
|
|
128
|
+
codex: ['.codex/hooks.json'],
|
|
129
|
+
windsurf: ['.windsurf/rules/ark.md'],
|
|
130
|
+
cline: ['.clinerules/ark.md'],
|
|
131
|
+
copilot: ['.github/copilot-instructions.md'],
|
|
132
|
+
kiro: ['.kiro/steering/ark.md'],
|
|
133
|
+
roo: ['.roo/rules/ark.md'],
|
|
134
|
+
continue: ['.continue/rules/ark.md'],
|
|
135
|
+
gemini: ['GEMINI.md'],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export function compactRouterHost(root) {
|
|
139
|
+
try {
|
|
140
|
+
const agents = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8');
|
|
141
|
+
return agents.match(COMPACT_ROUTER)?.[1] ?? null;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isCompactRouterAgentsContent(text) {
|
|
148
|
+
return typeof text === 'string' && COMPACT_ROUTER.test(text);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function hasCompactHostRegistration(root, host) {
|
|
152
|
+
if (host === 'none') return fs.existsSync(path.join(root, '.mcp.json'));
|
|
153
|
+
const files = COMPACT_HOST_FILES[host];
|
|
154
|
+
return Boolean(files) && files.every((relativePath) => fs.existsSync(path.join(root, relativePath)));
|
|
155
|
+
}
|
|
122
156
|
|
|
123
157
|
export function hasArkWorkflow(root) {
|
|
124
158
|
const workflowsDir = path.join(root, '.github', 'workflows');
|
|
@@ -141,9 +175,14 @@ export function hasArkWorkflow(root) {
|
|
|
141
175
|
}
|
|
142
176
|
|
|
143
177
|
export function missingGates(root) {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
178
|
+
const compactHost = compactRouterHost(root);
|
|
179
|
+
const required = compactHost
|
|
180
|
+
? REQUIRED_GATE_FILES.filter((relativePath) => relativePath !== '.mcp.json')
|
|
181
|
+
: REQUIRED_GATE_FILES;
|
|
182
|
+
const missing = required.filter((relativePath) => !fs.existsSync(path.join(root, relativePath)));
|
|
183
|
+
if (compactHost && !hasCompactHostRegistration(root, compactHost)) {
|
|
184
|
+
missing.push(`compact host registration (${compactHost})`);
|
|
185
|
+
}
|
|
147
186
|
if (!hasArkWorkflow(root)) missing.push(REQUIRED_GATE_WORKFLOW);
|
|
148
187
|
return missing;
|
|
149
188
|
}
|
package/bin/lib/graph-cycles.mjs
CHANGED
|
@@ -1,56 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
* Extracted from ark-check entry (R3).
|
|
4
|
-
*/
|
|
5
|
-
export function detectCycles(graph) {
|
|
6
|
-
let index = 0;
|
|
7
|
-
const indices = new Map();
|
|
8
|
-
const low = new Map();
|
|
9
|
-
const onStack = new Set();
|
|
10
|
-
const stack = [];
|
|
11
|
-
const components = [];
|
|
12
|
-
|
|
13
|
-
// ponytail: recursive Tarjan; make it iterative only if a real repo blows the stack.
|
|
14
|
-
const strongconnect = (v) => {
|
|
15
|
-
indices.set(v, index);
|
|
16
|
-
low.set(v, index);
|
|
17
|
-
index += 1;
|
|
18
|
-
stack.push(v);
|
|
19
|
-
onStack.add(v);
|
|
20
|
-
for (const w of [...(graph.get(v) ?? [])].sort()) {
|
|
21
|
-
if (!graph.has(w)) continue;
|
|
22
|
-
if (!indices.has(w)) {
|
|
23
|
-
strongconnect(w);
|
|
24
|
-
low.set(v, Math.min(low.get(v), low.get(w)));
|
|
25
|
-
} else if (onStack.has(w)) {
|
|
26
|
-
low.set(v, Math.min(low.get(v), indices.get(w)));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (low.get(v) === indices.get(v)) {
|
|
30
|
-
const comp = [];
|
|
31
|
-
let w;
|
|
32
|
-
do {
|
|
33
|
-
w = stack.pop();
|
|
34
|
-
onStack.delete(w);
|
|
35
|
-
comp.push(w);
|
|
36
|
-
} while (w !== v);
|
|
37
|
-
if (comp.length > 1) components.push(comp.sort());
|
|
38
|
-
}
|
|
39
|
-
};
|
|
1
|
+
/** Compatibility adapter; canonical Tarjan evaluation lives in the bundled Kernel engine. */
|
|
2
|
+
import { detectArchitectureCycles } from './analysis-engine.mjs';
|
|
40
3
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return components
|
|
46
|
-
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
47
|
-
.map((members) => ({
|
|
48
|
-
ruleId: 'CIRCULAR_DEPENDENCY',
|
|
49
|
-
file: members[0],
|
|
50
|
-
line: 1,
|
|
51
|
-
target: members.join(' → '),
|
|
52
|
-
message: `Circular dependency among ${members.length} files: ${members.join(' → ')} → ${members[0]}.`,
|
|
53
|
-
// Graph is value/runtime edges only (type-only imports omitted).
|
|
54
|
-
cycleKind: 'value',
|
|
55
|
-
}));
|
|
4
|
+
export function detectCycles(graph) {
|
|
5
|
+
return detectArchitectureCycles(graph);
|
|
56
6
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Host hook / MCP project templates for agent-gate install (Claude, Grok).
|
|
2
|
+
* Host hook / MCP project templates for agent-gate install (Claude, Grok, Codex).
|
|
3
3
|
* Kept out of agent-gates.mjs so install orchestration stays scannable (explore gap #5).
|
|
4
4
|
*/
|
|
5
5
|
import { execCommandParts, execRunner } from '../ark-shared.mjs';
|
|
@@ -41,6 +41,38 @@ export function claudeSettings(root) {
|
|
|
41
41
|
}, null, 2)}\n`;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
export function codexHooks(root) {
|
|
45
|
+
const runner = execRunner(root);
|
|
46
|
+
const codexRoot = '${CODEX_PROJECT_DIR:-${PWD:-.}}';
|
|
47
|
+
return `${JSON.stringify({
|
|
48
|
+
hooks: {
|
|
49
|
+
SessionStart: [
|
|
50
|
+
{
|
|
51
|
+
hooks: [
|
|
52
|
+
{
|
|
53
|
+
type: 'command',
|
|
54
|
+
timeout: 30,
|
|
55
|
+
command: `${runner} ${PREFERRED_MCP_BIN} --session-context --root "${codexRoot}" --config ark.config.json`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
PreToolUse: [
|
|
61
|
+
{
|
|
62
|
+
matcher: 'ApplyPatch|apply_patch|Write|Edit|MultiEdit',
|
|
63
|
+
hooks: [
|
|
64
|
+
{
|
|
65
|
+
type: 'command',
|
|
66
|
+
timeout: 30,
|
|
67
|
+
command: `${runner} ${PREFERRED_MCP_BIN} --hook --hook-repair --root "${codexRoot}" --config ark.config.json`,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
}, null, 2)}\n`;
|
|
74
|
+
}
|
|
75
|
+
|
|
44
76
|
// Grok Build project config: MCP registration (commit-friendly relative paths — unlike
|
|
45
77
|
// Codex's global config.toml, Grok loads .grok/config.toml from the project).
|
|
46
78
|
export function grokProjectConfig(root) {
|
|
@@ -36,7 +36,13 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
|
|
|
36
36
|
true
|
|
37
37
|
),
|
|
38
38
|
cursor: hostProfile('Cursor', null, null, false, false),
|
|
39
|
-
codex: hostProfile(
|
|
39
|
+
codex: hostProfile(
|
|
40
|
+
'OpenAI Codex',
|
|
41
|
+
'.codex/hooks.json',
|
|
42
|
+
'Best-effort PreToolUse `apply_patch`; Code Mode hosts may bypass the event',
|
|
43
|
+
false,
|
|
44
|
+
false
|
|
45
|
+
),
|
|
40
46
|
});
|
|
41
47
|
|
|
42
48
|
export const HOST_SUPPORT_HOSTS = Object.freeze(Object.keys(HOST_SUPPORT_MATRIX));
|
|
@@ -20,12 +20,14 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
PREFERRED_MCP_BIN,
|
|
22
22
|
claudeSettings,
|
|
23
|
+
codexHooks,
|
|
23
24
|
grokHooks,
|
|
24
25
|
grokProjectConfig,
|
|
25
26
|
} from './hook-templates.mjs';
|
|
26
27
|
import {
|
|
27
28
|
hasCheckArchitectureScript,
|
|
28
29
|
ensureTypecheckScript,
|
|
30
|
+
compactRouterHost,
|
|
29
31
|
writeTemplate,
|
|
30
32
|
} from './gate-files.mjs';
|
|
31
33
|
import {
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
detectCiNode,
|
|
37
39
|
cursorRule,
|
|
38
40
|
instructionRule,
|
|
41
|
+
compactAgentInstructions,
|
|
39
42
|
codexTomlSnippet,
|
|
40
43
|
arkCheckCommand,
|
|
41
44
|
checkArchitectureScriptSnippet,
|
|
@@ -215,12 +218,21 @@ export function runInstallAgentGates(args) {
|
|
|
215
218
|
}
|
|
216
219
|
const pm = packageManager(root);
|
|
217
220
|
const hasCheckScript = hasCheckArchitectureScript(root);
|
|
218
|
-
const { tools, source } =
|
|
221
|
+
const { tools, source } = args.compact && args.tools == null
|
|
222
|
+
? { tools: new Set(), source: 'compact-none' }
|
|
223
|
+
: resolveTools(args);
|
|
224
|
+
if (args.compact && tools.size > 1) {
|
|
225
|
+
console.error('--compact accepts exactly one selected host. Pass --tools <host>.');
|
|
226
|
+
process.exitCode = 2;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
219
229
|
const toolSource =
|
|
220
230
|
source === 'explicit'
|
|
221
231
|
? 'from --tools'
|
|
222
232
|
: source === 'detected'
|
|
223
233
|
? 'auto-detected from config dirs'
|
|
234
|
+
: source === 'compact-none'
|
|
235
|
+
? 'no active host detected'
|
|
224
236
|
: 'default set — no agent config dirs found';
|
|
225
237
|
console.log(`Agent gates for: ${[...tools].sort().join(', ')} (${toolSource})`);
|
|
226
238
|
const templates = [];
|
|
@@ -231,15 +243,21 @@ export function runInstallAgentGates(args) {
|
|
|
231
243
|
// Do not mutate package.json under --skills-only (typecheck bootstrap is gates/CI).
|
|
232
244
|
if (!args.skillsOnly) {
|
|
233
245
|
// Bootstrap typecheck before CI template so generated workflow includes the step.
|
|
234
|
-
const typecheckBootstrap = ensureTypecheckScript(root, { write:
|
|
235
|
-
if (typecheckBootstrap.changed && !args.json) {
|
|
246
|
+
const typecheckBootstrap = ensureTypecheckScript(root, { write: !args.compact });
|
|
247
|
+
if (typecheckBootstrap.changed && !args.compact && !args.json) {
|
|
236
248
|
console.log(
|
|
237
249
|
`Added package.json script "typecheck": "${typecheckBootstrap.script}" (tsconfig present; local/CI parity).`
|
|
238
250
|
);
|
|
239
251
|
}
|
|
240
252
|
// Base gates: tool-agnostic contract + CI backstop, always written.
|
|
241
|
-
|
|
242
|
-
templates.push([
|
|
253
|
+
const compactHost = args.compact ? [...tools][0] ?? null : null;
|
|
254
|
+
templates.push([
|
|
255
|
+
'AGENTS.md',
|
|
256
|
+
args.compact ? compactAgentInstructions(root, compactHost) : agentInstructions(root),
|
|
257
|
+
]);
|
|
258
|
+
if (!args.compact || !compactHost || compactHost === 'claude') {
|
|
259
|
+
templates.push(['.mcp.json', mcpJson(root)]);
|
|
260
|
+
}
|
|
243
261
|
templates.push([
|
|
244
262
|
'.github/workflows/ark-check.yml',
|
|
245
263
|
(() => {
|
|
@@ -252,13 +270,14 @@ export function runInstallAgentGates(args) {
|
|
|
252
270
|
]);
|
|
253
271
|
if (tools.has('cursor')) {
|
|
254
272
|
templates.push(['.cursor/mcp.json', mcpJson(root)]);
|
|
255
|
-
templates.push(['.cursor/rules/ark.mdc', cursorRule(root)]);
|
|
273
|
+
if (!args.compact) templates.push(['.cursor/rules/ark.mdc', cursorRule(root)]);
|
|
256
274
|
}
|
|
257
275
|
if (tools.has('claude')) {
|
|
258
276
|
templates.push(['.claude/settings.json', claudeSettings(root)]);
|
|
259
277
|
}
|
|
260
278
|
if (tools.has('codex')) {
|
|
261
|
-
templates.push(['
|
|
279
|
+
templates.push(['.codex/hooks.json', codexHooks(root)]);
|
|
280
|
+
if (!args.compact) templates.push(['docs/ark-codex-config.toml', codexTomlSnippet(root)]);
|
|
262
281
|
}
|
|
263
282
|
if (tools.has('grok')) {
|
|
264
283
|
templates.push(['.grok/config.toml', grokProjectConfig(root)]);
|
|
@@ -296,18 +315,37 @@ export function runInstallAgentGates(args) {
|
|
|
296
315
|
const version = arkPackageVersion();
|
|
297
316
|
const skills = skillTemplates().map(([name, content]) => [name, stampSkill(content, version)]);
|
|
298
317
|
const skillPaths = new Set();
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
318
|
+
if (!args.compact) {
|
|
319
|
+
for (const tool of tools) {
|
|
320
|
+
const target = SKILL_TOOL_TARGETS[tool];
|
|
321
|
+
if (!target) continue;
|
|
322
|
+
for (const [name, content] of skills) {
|
|
323
|
+
const relativePath = target(name);
|
|
324
|
+
skillPaths.add(relativePath);
|
|
325
|
+
templates.push([relativePath, content]);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// A compact router can be moved back from an explicit host removal. Delete the
|
|
331
|
+
// generic MCP file only when it exactly matches Ark's generated artifact.
|
|
332
|
+
const priorCompactHost = compactRouterHost(root);
|
|
333
|
+
if (args.compact && priorCompactHost === 'none' && [...tools][0]) {
|
|
334
|
+
const genericMcp = path.join(root, '.mcp.json');
|
|
335
|
+
try {
|
|
336
|
+
if (fs.readFileSync(genericMcp, 'utf8') === mcpJson(root)) fs.rmSync(genericMcp);
|
|
337
|
+
} catch {
|
|
338
|
+
// Missing or customized user MCP configuration is deliberately retained.
|
|
306
339
|
}
|
|
307
340
|
}
|
|
308
341
|
|
|
309
342
|
const results = templates.map(([relativePath, content]) =>
|
|
310
|
-
writeTemplate(
|
|
343
|
+
writeTemplate(
|
|
344
|
+
root,
|
|
345
|
+
relativePath,
|
|
346
|
+
content,
|
|
347
|
+
args.force || (args.compact && relativePath === 'AGENTS.md' && priorCompactHost !== null)
|
|
348
|
+
)
|
|
311
349
|
);
|
|
312
350
|
|
|
313
351
|
console.log('Ark agent gate templates:');
|
|
@@ -397,7 +435,7 @@ export function runInstallAgentGates(args) {
|
|
|
397
435
|
// developer's real config. A genuinely redirected CODEX_HOME or explicit
|
|
398
436
|
// --codex-home still wires as requested.
|
|
399
437
|
let codexMcp = null;
|
|
400
|
-
const wantCodexWire = tools.has('codex') || args.codexHome;
|
|
438
|
+
const wantCodexWire = !args.compact && (tools.has('codex') || args.codexHome);
|
|
401
439
|
const skipHomeWire =
|
|
402
440
|
wantCodexWire &&
|
|
403
441
|
isTempOrUpgradeRoot(root) &&
|
package/bin/lib/presets.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
applyFrameworkLayoutOverlays,
|
|
6
6
|
createElevenLayerConfig,
|
|
7
7
|
DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
|
|
8
|
+
discoverRepoUnits,
|
|
8
9
|
DEFAULT_INTENT_PREFIXES,
|
|
9
10
|
resolveIncludeRoots,
|
|
10
11
|
} from '../ark-shared.mjs';
|
|
@@ -203,12 +204,41 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
203
204
|
monorepo: (includeDirs, root) => {
|
|
204
205
|
let include =
|
|
205
206
|
includeDirs && includeDirs.length > 0 ? [...includeDirs] : [];
|
|
207
|
+
let units = [];
|
|
206
208
|
if (root) {
|
|
207
209
|
const resolved = resolveIncludeRoots(root);
|
|
208
210
|
if (resolved.length > 0) include = resolved;
|
|
211
|
+
units = discoverRepoUnits(root);
|
|
212
|
+
const nonProductRoots = new Set(
|
|
213
|
+
units
|
|
214
|
+
.filter((unit) => ['docs', 'example', 'test'].includes(unit.role))
|
|
215
|
+
.map((unit) => unit.root)
|
|
216
|
+
);
|
|
217
|
+
include = include.filter((entry) => !nonProductRoots.has(entry));
|
|
209
218
|
}
|
|
210
219
|
// Turborepo: apps/ + packages/; Nx enterprise: apps/ + libs/ (+ packages/).
|
|
211
220
|
if (include.length === 0) include = ['packages', 'apps', 'libs'];
|
|
221
|
+
// Established workspaces often have flat package source roots rather than a
|
|
222
|
+
// directory named domain/application. The package manifest supplies a real,
|
|
223
|
+
// reviewable role signal: a published library is a domain surface; an app or
|
|
224
|
+
// CLI package coordinates work. This is deliberately limited to package roots
|
|
225
|
+
// Ark already discovered, never a catch-all **/src/** fallback.
|
|
226
|
+
const packagePatterns = (roles) => {
|
|
227
|
+
if (!root) return [];
|
|
228
|
+
return units
|
|
229
|
+
.filter((unit) => roles.includes(unit.role))
|
|
230
|
+
.flatMap((unit) => unit.sourceRoots.map((sourceRoot) => {
|
|
231
|
+
const base = unit.root === '.'
|
|
232
|
+
? sourceRoot
|
|
233
|
+
: sourceRoot === '.'
|
|
234
|
+
? unit.root
|
|
235
|
+
: `${unit.root}/${sourceRoot}`;
|
|
236
|
+
return base === '.' ? null : `${base}/**`;
|
|
237
|
+
}))
|
|
238
|
+
.filter(Boolean);
|
|
239
|
+
};
|
|
240
|
+
const librarySourcePatterns = packagePatterns(['library']);
|
|
241
|
+
const applicationSourcePatterns = packagePatterns(['application', 'cli']);
|
|
212
242
|
return presetWithOverlays(
|
|
213
243
|
{
|
|
214
244
|
include,
|
|
@@ -219,14 +249,24 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
219
249
|
'Pure business rules and entities, in any package. No I/O, no framework, no ambient globals.',
|
|
220
250
|
// Domain by intentional folders only — NOT bare **/types.ts (that mis-classifies
|
|
221
251
|
// application bags like frontend/src/core/**/types.ts as Domain and creates false edges).
|
|
222
|
-
patterns: [
|
|
252
|
+
patterns: [
|
|
253
|
+
'**/domain/**',
|
|
254
|
+
'**/entities/**',
|
|
255
|
+
'**/cinematic/types.ts',
|
|
256
|
+
...librarySourcePatterns,
|
|
257
|
+
],
|
|
223
258
|
forbiddenGlobals: DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
|
|
224
259
|
optional: true,
|
|
225
260
|
},
|
|
226
261
|
{
|
|
227
262
|
name: 'ApplicationOrchestration',
|
|
228
263
|
description: 'Use cases and services that coordinate the domain through ports.',
|
|
229
|
-
patterns: [
|
|
264
|
+
patterns: [
|
|
265
|
+
'**/application/**',
|
|
266
|
+
'**/use-cases/**',
|
|
267
|
+
'**/services/**',
|
|
268
|
+
...applicationSourcePatterns,
|
|
269
|
+
],
|
|
230
270
|
optional: true,
|
|
231
271
|
},
|
|
232
272
|
{
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { globToRegExp } from '../ark-shared.mjs';
|
|
5
|
+
import { extractSemanticDependencies } from './analysis-engine.mjs';
|
|
5
6
|
import { lineOf } from './ast-scan.mjs';
|
|
6
7
|
import { normalize } from './scan-files.mjs';
|
|
7
8
|
|
|
@@ -104,9 +105,26 @@ export function collectSafetyDiagnostics(ts, root, config, files) {
|
|
|
104
105
|
|
|
105
106
|
for (const file of files) {
|
|
106
107
|
const source = fs.readFileSync(file, 'utf8');
|
|
108
|
+
// Most source files cannot contain a safety finding. Avoid parsing a second AST when the
|
|
109
|
+
// lexical markers for every supported diagnostic are absent; a match only opts into the
|
|
110
|
+
// existing exact AST analysis, so this cannot suppress a finding.
|
|
111
|
+
if (!/@ts-(?:ignore|nocheck)\b|\bany\b|\b(?:import|require)\s*\(|\barkgate(?:\/runtime)?\b/.test(source)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
107
114
|
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
|
|
108
115
|
const relFile = normalize(path.relative(root, file));
|
|
109
116
|
|
|
117
|
+
if (!matchesAny(relFile, dynamicAllowlist)) {
|
|
118
|
+
for (const dependency of extractSemanticDependencies(ts, sourceFile)) {
|
|
119
|
+
if (!dependency.unresolved) continue;
|
|
120
|
+
report.nonLiteralDynamicImports.push({
|
|
121
|
+
file: relFile,
|
|
122
|
+
line: dependency.line,
|
|
123
|
+
kind: dependency.kind === 'require' ? 'require' : 'import',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
110
128
|
for (const position of tsSuppressionPositions(sourceFile, source)) {
|
|
111
129
|
report.tsSuppressions.push({
|
|
112
130
|
file: relFile,
|
|
@@ -146,23 +164,6 @@ export function collectSafetyDiagnostics(ts, root, config, files) {
|
|
|
146
164
|
});
|
|
147
165
|
}
|
|
148
166
|
|
|
149
|
-
if (ts.isCallExpression(node)) {
|
|
150
|
-
const dynamicImport = node.expression?.kind === ts.SyntaxKind.ImportKeyword;
|
|
151
|
-
const directRequire =
|
|
152
|
-
ts.isIdentifier(node.expression) && node.expression.text === 'require';
|
|
153
|
-
const argument = node.arguments[0];
|
|
154
|
-
const unresolved =
|
|
155
|
-
(dynamicImport || directRequire) &&
|
|
156
|
-
(!argument || !ts.isStringLiteralLike(argument));
|
|
157
|
-
if (unresolved && !matchesAny(relFile, dynamicAllowlist)) {
|
|
158
|
-
report.nonLiteralDynamicImports.push({
|
|
159
|
-
file: relFile,
|
|
160
|
-
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
161
|
-
kind: directRequire ? 'require' : 'import',
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
167
|
if (!allowInMemory && !isProvider && ts.isImportDeclaration(node)) {
|
|
167
168
|
const specifier = node.moduleSpecifier;
|
|
168
169
|
const fromArk = ts.isStringLiteralLike(specifier) && /^arkgate(?:\/runtime)?$/.test(specifier.text);
|
package/bin/lib/scan-files.mjs
CHANGED
|
@@ -12,7 +12,7 @@ export const SOURCE_FILE_NAME = /\.[cm]?[tj]sx?$/;
|
|
|
12
12
|
* to production code (*.spec.ts). Counting them as ungoverned forces false
|
|
13
13
|
* CONFIG_UNCLASSIFIED_FILES under --strict-config on every starter. */
|
|
14
14
|
export const TEST_FILE_NAME =
|
|
15
|
-
|
|
15
|
+
/(^test(?:[-_.]).*|\.(spec|test)(?:-d)?\.)(tsx?|jsx?|mjsx?|cjsx?|mts|cts)$/i;
|
|
16
16
|
|
|
17
17
|
export function isGovernableSourceFile(name) {
|
|
18
18
|
return SOURCE_FILE_NAME.test(name) && !name.endsWith('.d.ts') && !TEST_FILE_NAME.test(name);
|
|
@@ -23,6 +23,17 @@ export function isSkippedSourceDir(name) {
|
|
|
23
23
|
name === 'node_modules' ||
|
|
24
24
|
name === 'dist' ||
|
|
25
25
|
name === 'coverage' ||
|
|
26
|
+
name === 'bench' ||
|
|
27
|
+
name === 'benches' ||
|
|
28
|
+
name === 'benchmark' ||
|
|
29
|
+
name === 'benchmarks' ||
|
|
30
|
+
name === 'docs' ||
|
|
31
|
+
name === 'documentation' ||
|
|
32
|
+
name === 'example' ||
|
|
33
|
+
name === 'examples' ||
|
|
34
|
+
name === 'fixture' ||
|
|
35
|
+
name === 'fixtures' ||
|
|
36
|
+
name === 'playground' ||
|
|
26
37
|
name === '__tests__' ||
|
|
27
38
|
name === '__mocks__' ||
|
|
28
39
|
name === 'e2e' ||
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { codexPromptsDir } from './codex-home.mjs';
|
|
7
|
-
import { __packageRoot, readJson } from './gate-files.mjs';
|
|
7
|
+
import { __packageRoot, isCompactRouterAgentsContent, readJson } from './gate-files.mjs';
|
|
8
8
|
|
|
9
9
|
export function normalizeToolsList(tools) {
|
|
10
10
|
if (tools == null) return [];
|
|
@@ -284,6 +284,13 @@ export function detectCodexHomeGap(root) {
|
|
|
284
284
|
|
|
285
285
|
export function detectSkillGaps(root) {
|
|
286
286
|
if (!fs.existsSync(path.join(root, 'AGENTS.md'))) return [];
|
|
287
|
+
try {
|
|
288
|
+
if (isCompactRouterAgentsContent(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8'))) {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
// Continue with the ordinary missing-skill detection below.
|
|
293
|
+
}
|
|
287
294
|
// The Ark source tree keeps the skill templates at templates/skills/ — it's the
|
|
288
295
|
// producer, not a consumer, so it must not nag itself to "install" its own skills.
|
|
289
296
|
if (fs.existsSync(path.join(root, 'templates', 'skills'))) return [];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Canonical algorithm: src/domain/sourcePolicy.ts
|
|
5
|
+
* Regenerate: node scripts/generate-cli-pure.mjs
|
|
6
|
+
* Drift check: node scripts/generate-cli-pure.mjs --check
|
|
7
|
+
*
|
|
8
|
+
* Pure CLI helper (bin/lib/source-policy.mjs). Zero Node I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const SOURCE_POLICY_MESSAGES = {
|
|
12
|
+
RAW_EVENT_PUBLISH: 'Publish through a registered intent creator; raw event objects or intent strings bypass Ark contracts and tooling.',
|
|
13
|
+
PUBLISH_MISSING_SOURCE: 'Strict Ark publish calls must include metadata.source.',
|
|
14
|
+
};
|
|
15
|
+
export function looksLikeArkIntent(value) {
|
|
16
|
+
return /^(Domain|Application|Adapter|Workflow|Job|Presentation|Reporting|Metadata|Security|Audit|Observability|Kernel)\.[A-Za-z0-9_.]+$/.test(value);
|
|
17
|
+
}
|
|
18
|
+
export function classifyPublishFacts(facts) {
|
|
19
|
+
if (!facts.publishCall)
|
|
20
|
+
return [];
|
|
21
|
+
const findings = [];
|
|
22
|
+
if ((facts.rawIntentName !== undefined && looksLikeArkIntent(facts.rawIntentName)) ||
|
|
23
|
+
facts.objectHasIntent) {
|
|
24
|
+
findings.push({
|
|
25
|
+
ruleId: 'RAW_EVENT_PUBLISH',
|
|
26
|
+
message: SOURCE_POLICY_MESSAGES.RAW_EVENT_PUBLISH,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (facts.arkPublishCandidate && !facts.hasSource) {
|
|
30
|
+
findings.push({
|
|
31
|
+
ruleId: 'PUBLISH_MISSING_SOURCE',
|
|
32
|
+
message: SOURCE_POLICY_MESSAGES.PUBLISH_MISSING_SOURCE,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return findings;
|
|
36
|
+
}
|