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.
Files changed (88) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/README.md +90 -51
  3. package/bin/ark-check.mjs +156 -39
  4. package/bin/ark-mcp.mjs +119 -6
  5. package/bin/ark-shared.mjs +216 -129
  6. package/bin/ark.mjs +134 -34
  7. package/bin/lib/adapter-contract.mjs +93 -0
  8. package/bin/lib/agent-gates.mjs +13 -0
  9. package/bin/lib/analysis-engine.mjs +1171 -0
  10. package/bin/lib/architecture-scan.mjs +84 -127
  11. package/bin/lib/ci-and-commands.mjs +40 -3
  12. package/bin/lib/codex-home.mjs +7 -0
  13. package/bin/lib/config-contract.mjs +331 -0
  14. package/bin/lib/config-warnings.mjs +7 -205
  15. package/bin/lib/doctor-plan.mjs +43 -16
  16. package/bin/lib/enforcement-profiles.mjs +97 -0
  17. package/bin/lib/field-install.mjs +67 -10
  18. package/bin/lib/gate-files.mjs +42 -3
  19. package/bin/lib/graph-cycles.mjs +4 -54
  20. package/bin/lib/hook-templates.mjs +33 -1
  21. package/bin/lib/host-support-matrix.mjs +83 -0
  22. package/bin/lib/install-migrate.mjs +99 -30
  23. package/bin/lib/mcp-adoption.mjs +35 -3
  24. package/bin/lib/open-html.mjs +75 -0
  25. package/bin/lib/presets.mjs +45 -4
  26. package/bin/lib/safety-diagnostics.mjs +36 -15
  27. package/bin/lib/scan-files.mjs +12 -1
  28. package/bin/lib/skill-install.mjs +72 -1
  29. package/bin/lib/source-policy.mjs +36 -0
  30. package/bin/lib/start-preview.mjs +271 -0
  31. package/bin/lib/ts-resolve.mjs +13 -3
  32. package/bin/lib/weakest-link.mjs +417 -0
  33. package/bin/lib/write-path-capabilities.mjs +186 -0
  34. package/bin/lib/write-path-detect.mjs +62 -99
  35. package/compat/nestjs.cjs +2 -0
  36. package/compat/nestjs.d.ts +2 -0
  37. package/compat/nestjs.js +1 -0
  38. package/compat/runtime.cjs +2 -0
  39. package/compat/runtime.d.ts +2 -0
  40. package/compat/runtime.js +1 -0
  41. package/dist/configContract-BxSIwVRo.d.cts +259 -0
  42. package/dist/configContract-BxSIwVRo.d.ts +259 -0
  43. package/dist/eslint/index.cjs +500 -61
  44. package/dist/eslint/index.d.cts +36 -20
  45. package/dist/eslint/index.d.ts +36 -20
  46. package/dist/eslint/index.js +500 -61
  47. package/dist/index.cjs +1349 -2741
  48. package/dist/index.d.cts +449 -483
  49. package/dist/index.d.ts +449 -483
  50. package/dist/index.js +1325 -2687
  51. package/docs/agent-guide.md +58 -34
  52. package/docs/ai-gates.md +79 -21
  53. package/docs/configuration.md +97 -0
  54. package/docs/enthusiast/README.md +3 -3
  55. package/docs/enthusiast/how-to-agent-gates.md +7 -3
  56. package/docs/migrate-from-ark-runtime-kernel.md +5 -3
  57. package/docs/package-surface.md +19 -19
  58. package/docs/production-hardening.md +31 -5
  59. package/docs/threat-model.md +65 -0
  60. package/docs/typescript-support.md +30 -3
  61. package/package.json +46 -11
  62. package/schemas/ark.analysis-result.schema.json +91 -0
  63. package/schemas/ark.config.schema.json +750 -0
  64. package/server.json +2 -2
  65. package/templates/hooks/pre-commit-ark +37 -0
  66. package/templates/skills/ark-architect.md +3 -2
  67. package/templates/skills/ark-coverage.md +2 -2
  68. package/templates/skills/ark-runtime.md +8 -5
  69. package/templates/skills/ark-upgrade.md +36 -16
  70. package/tests/fixtures/ts-consumer/ark.config.json +2 -0
  71. package/dist/eslint/index.cjs.map +0 -1
  72. package/dist/eslint/index.js.map +0 -1
  73. package/dist/index.cjs.map +0 -1
  74. package/dist/index.js.map +0 -1
  75. package/dist/nestjs/index.cjs +0 -2498
  76. package/dist/nestjs/index.cjs.map +0 -1
  77. package/dist/nestjs/index.d.cts +0 -22
  78. package/dist/nestjs/index.d.ts +0 -22
  79. package/dist/nestjs/index.js +0 -2474
  80. package/dist/nestjs/index.js.map +0 -1
  81. package/dist/runtime/index.cjs +0 -3352
  82. package/dist/runtime/index.cjs.map +0 -1
  83. package/dist/runtime/index.d.cts +0 -2
  84. package/dist/runtime/index.d.ts +0 -2
  85. package/dist/runtime/index.js +0 -3270
  86. package/dist/runtime/index.js.map +0 -1
  87. package/dist/types-BZ17b9i5.d.cts +0 -1068
  88. package/dist/types-BZ17b9i5.d.ts +0 -1068
@@ -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: assessed.gap.severity,
201
- message: assessed.gap.message,
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
+ }
@@ -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
- if (!root) return baseConfig;
53
- return applyFrameworkLayoutOverlays(baseConfig, root);
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: ['**/domain/**', '**/entities/**', '**/cinematic/types.ts'],
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: ['**/application/**', '**/use-cases/**', '**/services/**'],
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
- if (report.nonLiteralDynamicImports.length > 0) {
220
- const first = report.nonLiteralDynamicImports[0];
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: `${report.nonLiteralDynamicImports.length} non-literal dynamic import(s) cannot be resolved statically. Add only reviewed files to dynamicImportAllowlist.`,
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) {
@@ -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
- /\.(spec|test)\.(tsx?|jsx?|mts|cts)$/i;
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
+ }