arkgate 2.12.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 +122 -0
- package/README.md +90 -51
- package/bin/ark-check.mjs +156 -39
- package/bin/ark-mcp.mjs +119 -6
- package/bin/ark-shared.mjs +216 -129
- package/bin/ark.mjs +134 -34
- package/bin/lib/adapter-contract.mjs +93 -0
- package/bin/lib/agent-gates.mjs +13 -0
- package/bin/lib/analysis-engine.mjs +1171 -0
- package/bin/lib/architecture-scan.mjs +84 -127
- package/bin/lib/ci-and-commands.mjs +40 -3
- package/bin/lib/codex-home.mjs +7 -0
- package/bin/lib/config-contract.mjs +331 -0
- package/bin/lib/config-warnings.mjs +7 -205
- package/bin/lib/doctor-plan.mjs +43 -16
- package/bin/lib/enforcement-profiles.mjs +97 -0
- 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 +83 -0
- package/bin/lib/install-migrate.mjs +99 -30
- package/bin/lib/mcp-adoption.mjs +35 -3
- package/bin/lib/open-html.mjs +75 -0
- package/bin/lib/presets.mjs +45 -4
- package/bin/lib/safety-diagnostics.mjs +36 -15
- package/bin/lib/scan-files.mjs +12 -1
- package/bin/lib/skill-install.mjs +72 -1
- package/bin/lib/source-policy.mjs +36 -0
- package/bin/lib/start-preview.mjs +271 -0
- package/bin/lib/ts-resolve.mjs +13 -3
- package/bin/lib/weakest-link.mjs +417 -0
- package/bin/lib/write-path-capabilities.mjs +186 -0
- package/bin/lib/write-path-detect.mjs +62 -99
- 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 +500 -61
- package/dist/eslint/index.d.cts +36 -20
- package/dist/eslint/index.d.ts +36 -20
- package/dist/eslint/index.js +500 -61
- package/dist/index.cjs +1349 -2741
- package/dist/index.d.cts +449 -483
- package/dist/index.d.ts +449 -483
- package/dist/index.js +1325 -2687
- package/docs/agent-guide.md +58 -34
- package/docs/ai-gates.md +79 -21
- package/docs/configuration.md +97 -0
- package/docs/enthusiast/README.md +3 -3
- package/docs/enthusiast/how-to-agent-gates.md +7 -3
- package/docs/migrate-from-ark-runtime-kernel.md +5 -3
- package/docs/package-surface.md +19 -19
- package/docs/production-hardening.md +31 -5
- package/docs/threat-model.md +65 -0
- package/docs/typescript-support.md +30 -3
- package/package.json +46 -11
- package/schemas/ark.analysis-result.schema.json +91 -0
- package/schemas/ark.config.schema.json +750 -0
- package/server.json +2 -2
- package/templates/hooks/pre-commit-ark +37 -0
- package/templates/skills/ark-architect.md +3 -2
- package/templates/skills/ark-coverage.md +2 -2
- package/templates/skills/ark-runtime.md +8 -5
- package/templates/skills/ark-upgrade.md +36 -16
- package/tests/fixtures/ts-consumer/ark.config.json +2 -0
- 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 -2498
- package/dist/nestjs/index.cjs.map +0 -1
- package/dist/nestjs/index.d.cts +0 -22
- package/dist/nestjs/index.d.ts +0 -22
- package/dist/nestjs/index.js +0 -2474
- package/dist/nestjs/index.js.map +0 -1
- package/dist/runtime/index.cjs +0 -3352
- package/dist/runtime/index.cjs.map +0 -1
- package/dist/runtime/index.d.cts +0 -2
- package/dist/runtime/index.d.ts +0 -2
- package/dist/runtime/index.js +0 -3270
- package/dist/runtime/index.js.map +0 -1
- package/dist/types-BZ17b9i5.d.cts +0 -1068
- package/dist/types-BZ17b9i5.d.ts +0 -1068
package/bin/lib/mcp-adoption.mjs
CHANGED
|
@@ -9,8 +9,9 @@ import { CORE_LAYER_NAMES } from './core-layers.mjs';
|
|
|
9
9
|
import { falseGreenAdoptionGap } from './field-install.mjs';
|
|
10
10
|
import { assessCodexHomeMcp, codexConfigPath } from './codex-home.mjs';
|
|
11
11
|
import { detectWritePathCapabilities } from './write-path-detect.mjs';
|
|
12
|
-
import { skillTemplateNames } from './skill-install.mjs';
|
|
12
|
+
import { detectActiveAgentHost, skillTemplateNames } from './skill-install.mjs';
|
|
13
13
|
import { detectDeployPathQuality } from './deploy-path.mjs';
|
|
14
|
+
import { collectWeakestLinkGaps } from './weakest-link.mjs';
|
|
14
15
|
|
|
15
16
|
export { detectDeployPathQuality };
|
|
16
17
|
|
|
@@ -195,10 +196,22 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
195
196
|
scopedTable: assessed.scopedTable,
|
|
196
197
|
};
|
|
197
198
|
if (assessed.gap) {
|
|
199
|
+
// Temp/upgrade MCP roots stay urgent (fail-closed). Non-temp Codex-home debt
|
|
200
|
+
// is deferred only when the session host is known and is not Codex
|
|
201
|
+
// (Grok/Claude/Cursor…). Unknown host (CI/plain shell) keeps original severity.
|
|
202
|
+
const tempUrgent = Boolean(assessed.tempPath);
|
|
203
|
+
const activeHost = detectActiveAgentHost();
|
|
204
|
+
const deferred =
|
|
205
|
+
!tempUrgent && activeHost != null && activeHost !== 'codex';
|
|
206
|
+
const severity = deferred ? 'info' : assessed.gap.severity;
|
|
207
|
+
const message = deferred
|
|
208
|
+
? `Deferred (fix when using Codex): ${assessed.gap.message}`
|
|
209
|
+
: assessed.gap.message;
|
|
198
210
|
gaps.push({
|
|
199
211
|
id: assessed.gap.id,
|
|
200
|
-
severity
|
|
201
|
-
|
|
212
|
+
severity,
|
|
213
|
+
deferred,
|
|
214
|
+
message,
|
|
202
215
|
fix: arkCommand(root, 'ark-check', assessed.gap.fixArgs),
|
|
203
216
|
});
|
|
204
217
|
}
|
|
@@ -403,6 +416,19 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
403
416
|
}
|
|
404
417
|
}
|
|
405
418
|
|
|
419
|
+
// --- Q3 weakest-link: CI / pre-commit / config drift (local FS; optional GH via ARK_DOCTOR_GITHUB=1) ---
|
|
420
|
+
const includeGithub =
|
|
421
|
+
process.env.ARK_DOCTOR_GITHUB === '1' ||
|
|
422
|
+
process.env.ARK_DOCTOR_GITHUB === 'true';
|
|
423
|
+
const weakest = collectWeakestLinkGaps(root, {
|
|
424
|
+
adopted,
|
|
425
|
+
isProducer,
|
|
426
|
+
includeGithub,
|
|
427
|
+
});
|
|
428
|
+
for (const g of weakest.gaps) {
|
|
429
|
+
gaps.push(g);
|
|
430
|
+
}
|
|
431
|
+
|
|
406
432
|
return {
|
|
407
433
|
gaps,
|
|
408
434
|
hosts,
|
|
@@ -415,6 +441,12 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
415
441
|
deployPath,
|
|
416
442
|
contractFalseGreen,
|
|
417
443
|
writePath,
|
|
444
|
+
enforcement: {
|
|
445
|
+
ci: weakest.ci,
|
|
446
|
+
preCommit: weakest.preCommit,
|
|
447
|
+
drift: weakest.drift,
|
|
448
|
+
github: weakest.github,
|
|
449
|
+
},
|
|
418
450
|
};
|
|
419
451
|
}
|
|
420
452
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a local HTML file in the default browser (macOS / Windows / Linux).
|
|
3
|
+
* Used after ark-check --report writes a showcase/beginner report.
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether report generation should open the browser.
|
|
12
|
+
* Default: yes for interactive local TTY; never in CI/Vitest unless forced.
|
|
13
|
+
*
|
|
14
|
+
* @param {{ force?: boolean, noOpen?: boolean, env?: NodeJS.ProcessEnv, isTty?: boolean }} [opts]
|
|
15
|
+
*/
|
|
16
|
+
export function shouldOpenHtmlReport(opts = {}) {
|
|
17
|
+
const env = opts.env ?? process.env;
|
|
18
|
+
if (opts.noOpen) return false;
|
|
19
|
+
if (opts.force) return true;
|
|
20
|
+
const off = env.ARK_NO_OPEN_REPORT;
|
|
21
|
+
if (off === '1' || off === 'true' || off === 'yes') return false;
|
|
22
|
+
if (env.CI === 'true' || env.CI === '1') return false;
|
|
23
|
+
if (env.GITHUB_ACTIONS === 'true') return false;
|
|
24
|
+
if (env.VITEST) return false;
|
|
25
|
+
const tty = opts.isTty ?? Boolean(process.stdout.isTTY);
|
|
26
|
+
return tty;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Launch the OS default browser for a local file path (non-blocking).
|
|
31
|
+
* Errors are swallowed — report generation must not fail if the browser cannot open.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} filePath
|
|
34
|
+
* @param {{ platform?: NodeJS.Platform, spawn?: typeof spawn }} [opts]
|
|
35
|
+
* @returns {{ ok: boolean, command?: string, reason?: string }}
|
|
36
|
+
*/
|
|
37
|
+
export function openHtmlInBrowser(filePath, opts = {}) {
|
|
38
|
+
const abs = path.resolve(filePath);
|
|
39
|
+
if (!fs.existsSync(abs)) {
|
|
40
|
+
return { ok: false, reason: 'missing-file' };
|
|
41
|
+
}
|
|
42
|
+
const platform = opts.platform ?? process.platform;
|
|
43
|
+
const spawnFn = opts.spawn ?? spawn;
|
|
44
|
+
|
|
45
|
+
/** @type {string} */
|
|
46
|
+
let command;
|
|
47
|
+
/** @type {string[]} */
|
|
48
|
+
let args;
|
|
49
|
+
if (platform === 'darwin') {
|
|
50
|
+
command = 'open';
|
|
51
|
+
args = [abs];
|
|
52
|
+
} else if (platform === 'win32') {
|
|
53
|
+
command = 'rundll32.exe';
|
|
54
|
+
args = ['url.dll,FileProtocolHandler', pathToFileURL(abs).href];
|
|
55
|
+
} else {
|
|
56
|
+
command = 'xdg-open';
|
|
57
|
+
args = [abs];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const child = spawnFn(command, args, {
|
|
62
|
+
detached: true,
|
|
63
|
+
stdio: 'ignore',
|
|
64
|
+
windowsHide: true,
|
|
65
|
+
shell: false,
|
|
66
|
+
});
|
|
67
|
+
child.on('error', () => {
|
|
68
|
+
/* browser missing (e.g. headless Linux) — ignore */
|
|
69
|
+
});
|
|
70
|
+
child.unref();
|
|
71
|
+
return { ok: true, command };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
74
|
+
}
|
|
75
|
+
}
|
package/bin/lib/presets.mjs
CHANGED
|
@@ -5,9 +5,11 @@ 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';
|
|
12
|
+
import { withArkConfigMetadata } from './config-contract.mjs';
|
|
11
13
|
|
|
12
14
|
export function denyUpward(names) {
|
|
13
15
|
const rules = [];
|
|
@@ -49,8 +51,8 @@ export function peerIsolationEdges(layerNames, sliceFolders, message) {
|
|
|
49
51
|
// (that carves out legitimate `src/shared/kernel/**` SharedKernel paths).
|
|
50
52
|
export const FRAMEWORK_INTERNAL_EXCLUDE = ['src/kernel/**', '**/src/kernel/**'];
|
|
51
53
|
export function presetWithOverlays(baseConfig, root) {
|
|
52
|
-
|
|
53
|
-
return
|
|
54
|
+
const config = root ? applyFrameworkLayoutOverlays(baseConfig, root) : baseConfig;
|
|
55
|
+
return withArkConfigMetadata(config);
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export const ARCHITECTURE_PRESETS = {
|
|
@@ -202,12 +204,41 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
202
204
|
monorepo: (includeDirs, root) => {
|
|
203
205
|
let include =
|
|
204
206
|
includeDirs && includeDirs.length > 0 ? [...includeDirs] : [];
|
|
207
|
+
let units = [];
|
|
205
208
|
if (root) {
|
|
206
209
|
const resolved = resolveIncludeRoots(root);
|
|
207
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));
|
|
208
218
|
}
|
|
209
219
|
// Turborepo: apps/ + packages/; Nx enterprise: apps/ + libs/ (+ packages/).
|
|
210
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']);
|
|
211
242
|
return presetWithOverlays(
|
|
212
243
|
{
|
|
213
244
|
include,
|
|
@@ -218,14 +249,24 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
218
249
|
'Pure business rules and entities, in any package. No I/O, no framework, no ambient globals.',
|
|
219
250
|
// Domain by intentional folders only — NOT bare **/types.ts (that mis-classifies
|
|
220
251
|
// application bags like frontend/src/core/**/types.ts as Domain and creates false edges).
|
|
221
|
-
patterns: [
|
|
252
|
+
patterns: [
|
|
253
|
+
'**/domain/**',
|
|
254
|
+
'**/entities/**',
|
|
255
|
+
'**/cinematic/types.ts',
|
|
256
|
+
...librarySourcePatterns,
|
|
257
|
+
],
|
|
222
258
|
forbiddenGlobals: DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
|
|
223
259
|
optional: true,
|
|
224
260
|
},
|
|
225
261
|
{
|
|
226
262
|
name: 'ApplicationOrchestration',
|
|
227
263
|
description: 'Use cases and services that coordinate the domain through ports.',
|
|
228
|
-
patterns: [
|
|
264
|
+
patterns: [
|
|
265
|
+
'**/application/**',
|
|
266
|
+
'**/use-cases/**',
|
|
267
|
+
'**/services/**',
|
|
268
|
+
...applicationSourcePatterns,
|
|
269
|
+
],
|
|
229
270
|
optional: true,
|
|
230
271
|
},
|
|
231
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,18 +164,6 @@ export function collectSafetyDiagnostics(ts, root, config, files) {
|
|
|
146
164
|
});
|
|
147
165
|
}
|
|
148
166
|
|
|
149
|
-
if (ts.isCallExpression(node) && node.expression?.kind === ts.SyntaxKind.ImportKeyword) {
|
|
150
|
-
const argument = node.arguments[0];
|
|
151
|
-
if (!argument || !ts.isStringLiteralLike(argument)) {
|
|
152
|
-
if (!matchesAny(relFile, dynamicAllowlist)) {
|
|
153
|
-
report.nonLiteralDynamicImports.push({
|
|
154
|
-
file: relFile,
|
|
155
|
-
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
167
|
if (!allowInMemory && !isProvider && ts.isImportDeclaration(node)) {
|
|
162
168
|
const specifier = node.moduleSpecifier;
|
|
163
169
|
const fromArk = ts.isStringLiteralLike(specifier) && /^arkgate(?:\/runtime)?$/.test(specifier.text);
|
|
@@ -216,13 +222,28 @@ export function collectSafetyDiagnostics(ts, root, config, files) {
|
|
|
216
222
|
}
|
|
217
223
|
|
|
218
224
|
const warnings = [];
|
|
219
|
-
|
|
220
|
-
|
|
225
|
+
const nonLiteralImports = report.nonLiteralDynamicImports.filter(
|
|
226
|
+
(entry) => entry.kind !== 'require'
|
|
227
|
+
);
|
|
228
|
+
if (nonLiteralImports.length > 0) {
|
|
229
|
+
const first = nonLiteralImports[0];
|
|
221
230
|
warnings.push({
|
|
222
231
|
ruleId: 'DYNAMIC_IMPORT_NOT_ALLOWLISTED',
|
|
223
232
|
file: first.file,
|
|
224
233
|
line: first.line,
|
|
225
|
-
message: `${
|
|
234
|
+
message: `${nonLiteralImports.length} non-literal dynamic import(s) cannot be resolved statically. Add only reviewed files to dynamicImportAllowlist.`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const nonLiteralRequires = report.nonLiteralDynamicImports.filter(
|
|
238
|
+
(entry) => entry.kind === 'require'
|
|
239
|
+
);
|
|
240
|
+
if (nonLiteralRequires.length > 0) {
|
|
241
|
+
const first = nonLiteralRequires[0];
|
|
242
|
+
warnings.push({
|
|
243
|
+
ruleId: 'DYNAMIC_REQUIRE_NOT_ALLOWLISTED',
|
|
244
|
+
file: first.file,
|
|
245
|
+
line: first.line,
|
|
246
|
+
message: `${nonLiteralRequires.length} non-literal require call(s) cannot be resolved statically. Add only reviewed files to dynamicImportAllowlist.`,
|
|
226
247
|
});
|
|
227
248
|
}
|
|
228
249
|
if (report.tsSuppressions.length > maxTsSuppressions) {
|
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 [];
|
|
@@ -23,6 +23,70 @@ export function normalizeToolsList(tools) {
|
|
|
23
23
|
return [];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function envTruthy(v) {
|
|
27
|
+
if (v == null || v === '') return false;
|
|
28
|
+
const s = String(v).trim().toLowerCase();
|
|
29
|
+
return s !== '0' && s !== 'false' && s !== 'no' && s !== 'off';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Best-effort active agent host for this process (session host).
|
|
34
|
+
* Prefer ARK_ACTIVE_HOST when set. Do NOT treat CODEX_HOME alone as Codex —
|
|
35
|
+
* that dir exists for anyone who installed Codex, even when running Grok/Claude.
|
|
36
|
+
*
|
|
37
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
38
|
+
* @returns {string|null} tool id (claude|cursor|codex|grok|…) or null if unknown
|
|
39
|
+
*/
|
|
40
|
+
export function detectActiveAgentHost(env = process.env) {
|
|
41
|
+
const explicit = String(env.ARK_ACTIVE_HOST || '')
|
|
42
|
+
.trim()
|
|
43
|
+
.toLowerCase();
|
|
44
|
+
if (explicit) return explicit;
|
|
45
|
+
|
|
46
|
+
// Grok / xAI Build
|
|
47
|
+
if (
|
|
48
|
+
envTruthy(env.GROK_BUILD) ||
|
|
49
|
+
envTruthy(env.XAI_GROK) ||
|
|
50
|
+
env.GROK_WORKSPACE_ROOT ||
|
|
51
|
+
env.GROK_SESSION_ID
|
|
52
|
+
) {
|
|
53
|
+
return 'grok';
|
|
54
|
+
}
|
|
55
|
+
// Claude Code
|
|
56
|
+
if (
|
|
57
|
+
env.CLAUDE_PROJECT_DIR ||
|
|
58
|
+
envTruthy(env.CLAUDE_CODE) ||
|
|
59
|
+
envTruthy(env.CLAUDECODE) ||
|
|
60
|
+
env.CLAUDE_CODE_ENTRYPOINT
|
|
61
|
+
) {
|
|
62
|
+
return 'claude';
|
|
63
|
+
}
|
|
64
|
+
// Cursor agent
|
|
65
|
+
if (env.CURSOR_TRACE_ID || env.CURSOR_AGENT || envTruthy(env.CURSOR_AGENT_CLI)) {
|
|
66
|
+
return 'cursor';
|
|
67
|
+
}
|
|
68
|
+
// Codex session — never CODEX_HOME alone (see above)
|
|
69
|
+
if (
|
|
70
|
+
envTruthy(env.CODEX_SANDBOX) ||
|
|
71
|
+
env.CODEX_THREAD_ID ||
|
|
72
|
+
envTruthy(env.CODEX_CI) ||
|
|
73
|
+
env.CODEX_SESSION_ID
|
|
74
|
+
) {
|
|
75
|
+
return 'codex';
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* True when Codex home / MCP / prompts should be treated as an urgent concern
|
|
82
|
+
* for this process. Non-Codex hosts (Grok, Claude, Cursor, …) defer Codex debt.
|
|
83
|
+
*
|
|
84
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
85
|
+
*/
|
|
86
|
+
export function codexConcernIsActive(env = process.env) {
|
|
87
|
+
return detectActiveAgentHost(env) === 'codex';
|
|
88
|
+
}
|
|
89
|
+
|
|
26
90
|
export function resolveTools(args) {
|
|
27
91
|
const explicit = normalizeToolsList(args.tools);
|
|
28
92
|
if (explicit.length > 0) {
|
|
@@ -220,6 +284,13 @@ export function detectCodexHomeGap(root) {
|
|
|
220
284
|
|
|
221
285
|
export function detectSkillGaps(root) {
|
|
222
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
|
+
}
|
|
223
294
|
// The Ark source tree keeps the skill templates at templates/skills/ — it's the
|
|
224
295
|
// producer, not a consumer, so it must not nag itself to "install" its own skills.
|
|
225
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
|
+
}
|