arkgate 2.3.0 → 2.4.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 +44 -0
- package/README.md +26 -16
- package/SECURITY.md +9 -8
- package/bin/ark-check.mjs +599 -53
- package/bin/ark-shared.mjs +53 -0
- package/bin/ark.mjs +20 -5
- 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 +11 -5
- package/docs/ai-gates.md +11 -7
- package/docs/brownfield-adoption.md +14 -13
- package/docs/demos/03-copilot-autopilot.md +5 -3
- package/docs/enthusiast/README.md +4 -3
- package/docs/enthusiast/how-to-agent-gates.md +14 -6
- package/docs/enthusiast/reference-commands.md +23 -8
- package/docs/migrate-from-ark-runtime-kernel.md +18 -0
- package/docs/typescript-support.md +142 -0
- package/package.json +12 -3
- package/server.json +2 -2
- package/templates/skills/ark-autopilot.md +6 -4
- package/templates/skills/ark-explain.md +7 -2
- package/templates/skills/ark-fix.md +16 -12
- package/templates/skills/ark-loop.md +14 -4
- package/templates/skills/ark-upgrade.md +26 -1
- package/templates/tests/ark-adoption-gaps.test.ts +68 -0
- package/tests/fixtures/ts-consumer/ark.config.json +11 -0
- package/tests/fixtures/ts-consumer/src/app/types.ts +1 -0
- package/tests/fixtures/ts-consumer/src/domain/bad.ts +1 -0
- package/tests/fixtures/ts-consumer/src/domain/ok.ts +1 -0
- package/tests/fixtures/ts-consumer/src/domain/user.ts +1 -0
- package/tests/fixtures/ts-consumer/tsconfig.json +16 -0
package/bin/ark-check.mjs
CHANGED
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
resolveIntentLayer,
|
|
38
38
|
resolveOperatingMode,
|
|
39
39
|
shouldShowNewHereNudge,
|
|
40
|
+
usableTypescript,
|
|
41
|
+
typescriptUsabilityHint,
|
|
40
42
|
} from './ark-shared.mjs';
|
|
41
43
|
|
|
42
44
|
function parseArgs(argv) {
|
|
@@ -1051,49 +1053,89 @@ function writeTemplate(root, relativePath, content, force) {
|
|
|
1051
1053
|
}
|
|
1052
1054
|
|
|
1053
1055
|
/**
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
if (ts && typeof ts === 'object' && ts.sys && typeof ts.sys.fileExists === 'function') {
|
|
1062
|
-
return ts;
|
|
1063
|
-
}
|
|
1064
|
-
return null;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
/**
|
|
1068
|
-
* Load a TypeScript module with a working `sys`. Prefer the project's install when it is
|
|
1069
|
-
* API-compatible; otherwise Ark's dependency (or a bare import). Returns null only when
|
|
1070
|
-
* nothing usable is available.
|
|
1056
|
+
* Load a TypeScript module with a working JS API host (`sys` + AST + resolve).
|
|
1057
|
+
* Prefer the project's install when API-compatible (TS 5/6 + any TS 7 that still
|
|
1058
|
+
* exposes the classic JS host). TypeScript 7.0.x main entry is version-only
|
|
1059
|
+
* (`{ version, versionMajorMinor }`); programmatic APIs live under
|
|
1060
|
+
* `typescript/unstable/*` and are not yet the gate's host — we fall through to
|
|
1061
|
+
* ArkGate's own `typescript` dependency (JS-API 5.x) or a bare import.
|
|
1062
|
+
* Returns `{ ts, source, version, fallbackReason? }` or null.
|
|
1071
1063
|
*/
|
|
1072
1064
|
async function loadTypeScript(root) {
|
|
1065
|
+
const { createRequire } = await import('node:module');
|
|
1073
1066
|
const loaders = [];
|
|
1074
1067
|
try {
|
|
1075
|
-
const { createRequire } = await import('node:module');
|
|
1076
1068
|
const req = createRequire(path.join(root, 'package.json'));
|
|
1077
|
-
loaders.push(
|
|
1069
|
+
loaders.push({
|
|
1070
|
+
label: 'project',
|
|
1071
|
+
load: () => req('typescript'),
|
|
1072
|
+
resolvePath: () => {
|
|
1073
|
+
try {
|
|
1074
|
+
return req.resolve('typescript');
|
|
1075
|
+
} catch {
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
1078
1080
|
} catch {
|
|
1079
1081
|
/* project has no package.json resolvable tree */
|
|
1080
1082
|
}
|
|
1083
|
+
// Nested under arkgate (production dependency) — must work when project has only TS7.
|
|
1081
1084
|
try {
|
|
1082
|
-
const { createRequire } = await import('node:module');
|
|
1083
1085
|
const req = createRequire(__arkCheckCli);
|
|
1084
|
-
loaders.push(
|
|
1086
|
+
loaders.push({
|
|
1087
|
+
label: 'arkgate',
|
|
1088
|
+
load: () => req('typescript'),
|
|
1089
|
+
resolvePath: () => {
|
|
1090
|
+
try {
|
|
1091
|
+
return req.resolve('typescript');
|
|
1092
|
+
} catch {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1085
1097
|
} catch {
|
|
1086
1098
|
/* ark install tree unavailable */
|
|
1087
1099
|
}
|
|
1088
|
-
loaders.push(
|
|
1089
|
-
|
|
1090
|
-
|
|
1100
|
+
loaders.push({
|
|
1101
|
+
label: 'import',
|
|
1102
|
+
load: async () => {
|
|
1103
|
+
const m = await import('typescript');
|
|
1104
|
+
return m;
|
|
1105
|
+
},
|
|
1106
|
+
resolvePath: () => null,
|
|
1091
1107
|
});
|
|
1092
1108
|
|
|
1093
|
-
|
|
1109
|
+
let projectRejected = null;
|
|
1110
|
+
const triedPaths = new Set();
|
|
1111
|
+
for (const { label, load, resolvePath } of loaders) {
|
|
1094
1112
|
try {
|
|
1095
|
-
const
|
|
1096
|
-
if (
|
|
1113
|
+
const resolved = typeof resolvePath === 'function' ? resolvePath() : null;
|
|
1114
|
+
if (resolved && triedPaths.has(resolved)) {
|
|
1115
|
+
// Same physical package already rejected (e.g. project === hoisted arkgate path).
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (resolved) triedPaths.add(resolved);
|
|
1119
|
+
|
|
1120
|
+
const mod = await load();
|
|
1121
|
+
const ts = usableTypescript(mod);
|
|
1122
|
+
if (ts) {
|
|
1123
|
+
const version =
|
|
1124
|
+
typeof ts.version === 'string'
|
|
1125
|
+
? ts.version
|
|
1126
|
+
: typeof mod?.version === 'string'
|
|
1127
|
+
? mod.version
|
|
1128
|
+
: undefined;
|
|
1129
|
+
return {
|
|
1130
|
+
ts,
|
|
1131
|
+
source: label,
|
|
1132
|
+
version,
|
|
1133
|
+
...(projectRejected ? { fallbackReason: projectRejected } : {}),
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
if (label === 'project' && mod) {
|
|
1137
|
+
projectRejected = `project typescript is not API-compatible (${typescriptUsabilityHint(mod)}); using ArkGate's JS-API TypeScript fallback (TypeScript 7.0 main export is version-only). See docs/typescript-support.md.`;
|
|
1138
|
+
}
|
|
1097
1139
|
} catch {
|
|
1098
1140
|
/* try next loader */
|
|
1099
1141
|
}
|
|
@@ -1202,7 +1244,8 @@ function mcpJson(root) {
|
|
|
1202
1244
|
mcpServers: {
|
|
1203
1245
|
ark: {
|
|
1204
1246
|
type: 'stdio',
|
|
1205
|
-
|
|
1247
|
+
// Prefer arkgate-mcp; ark-mcp alias still works for one major.
|
|
1248
|
+
...execCommandParts(root, PREFERRED_MCP_BIN, ['--root', '.', '--config', 'ark.config.json']),
|
|
1206
1249
|
},
|
|
1207
1250
|
},
|
|
1208
1251
|
}, null, 2)}\n`;
|
|
@@ -1213,7 +1256,7 @@ function mcpJson(root) {
|
|
|
1213
1256
|
// it flags the two gotchas of hand-editing the global config: absolute paths (config.toml is
|
|
1214
1257
|
// loaded without the project as cwd) and the required restart.
|
|
1215
1258
|
function codexTomlSnippet(root) {
|
|
1216
|
-
const { command, args } = execCommandParts(root,
|
|
1259
|
+
const { command, args } = execCommandParts(root, PREFERRED_MCP_BIN, [
|
|
1217
1260
|
'--root',
|
|
1218
1261
|
'/absolute/path/to/project',
|
|
1219
1262
|
'--config',
|
|
@@ -1347,7 +1390,7 @@ function claudeSettings(root) {
|
|
|
1347
1390
|
hooks: [
|
|
1348
1391
|
{
|
|
1349
1392
|
type: 'command',
|
|
1350
|
-
command: `${runner}
|
|
1393
|
+
command: `${runner} ${PREFERRED_MCP_BIN} --session-context --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`,
|
|
1351
1394
|
},
|
|
1352
1395
|
],
|
|
1353
1396
|
},
|
|
@@ -1358,7 +1401,7 @@ function claudeSettings(root) {
|
|
|
1358
1401
|
hooks: [
|
|
1359
1402
|
{
|
|
1360
1403
|
type: 'command',
|
|
1361
|
-
command: `${runner}
|
|
1404
|
+
command: `${runner} ${PREFERRED_MCP_BIN} --hook --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`,
|
|
1362
1405
|
},
|
|
1363
1406
|
],
|
|
1364
1407
|
},
|
|
@@ -1370,7 +1413,7 @@ function claudeSettings(root) {
|
|
|
1370
1413
|
// Grok Build project config: MCP registration (commit-friendly relative paths — unlike
|
|
1371
1414
|
// Codex's global config.toml, Grok loads .grok/config.toml from the project).
|
|
1372
1415
|
function grokProjectConfig(root) {
|
|
1373
|
-
const { command, args } = execCommandParts(root,
|
|
1416
|
+
const { command, args } = execCommandParts(root, PREFERRED_MCP_BIN, [
|
|
1374
1417
|
'--root',
|
|
1375
1418
|
'.',
|
|
1376
1419
|
'--config',
|
|
@@ -1385,7 +1428,7 @@ args = [${argsToml}]
|
|
|
1385
1428
|
`;
|
|
1386
1429
|
}
|
|
1387
1430
|
|
|
1388
|
-
// Grok Build hooks: same
|
|
1431
|
+
// Grok Build hooks: same arkgate-mcp contracts as Claude. Grok sets CLAUDE_PROJECT_DIR as
|
|
1389
1432
|
// an alias for GROK_WORKSPACE_ROOT. Matcher keeps Claude names (Write|Edit|MultiEdit)
|
|
1390
1433
|
// and Grok natives (write|search_replace) — Grok aliases both directions.
|
|
1391
1434
|
function grokHooks(root) {
|
|
@@ -1398,7 +1441,7 @@ function grokHooks(root) {
|
|
|
1398
1441
|
{
|
|
1399
1442
|
type: 'command',
|
|
1400
1443
|
timeout: 30,
|
|
1401
|
-
command: `${runner}
|
|
1444
|
+
command: `${runner} ${PREFERRED_MCP_BIN} --session-context --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`,
|
|
1402
1445
|
},
|
|
1403
1446
|
],
|
|
1404
1447
|
},
|
|
@@ -1410,7 +1453,7 @@ function grokHooks(root) {
|
|
|
1410
1453
|
{
|
|
1411
1454
|
type: 'command',
|
|
1412
1455
|
timeout: 30,
|
|
1413
|
-
command: `${runner}
|
|
1456
|
+
command: `${runner} ${PREFERRED_MCP_BIN} --hook --root "$CLAUDE_PROJECT_DIR" --config ark.config.json`,
|
|
1414
1457
|
},
|
|
1415
1458
|
],
|
|
1416
1459
|
},
|
|
@@ -1622,7 +1665,9 @@ function wireCodexMcp(root, force) {
|
|
|
1622
1665
|
const esc = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
1623
1666
|
const absRoot = path.resolve(root);
|
|
1624
1667
|
const absConfig = path.join(absRoot, 'ark.config.json');
|
|
1625
|
-
|
|
1668
|
+
// Preferred product bin; absolute --root so Codex (cwd ≠ project) resolves correctly.
|
|
1669
|
+
const preferredBin = 'arkgate-mcp';
|
|
1670
|
+
const { command, args } = execCommandParts(root, preferredBin, [
|
|
1626
1671
|
'--root',
|
|
1627
1672
|
esc(absRoot),
|
|
1628
1673
|
'--config',
|
|
@@ -1640,7 +1685,9 @@ args = [${argsToml}]`;
|
|
|
1640
1685
|
}
|
|
1641
1686
|
const tableRe = /(^|\n)\[mcp_servers\.ark\][^\n]*\n(?:(?!\[)[^\n]*\n?)*/;
|
|
1642
1687
|
const hasTable = tableRe.test(existing);
|
|
1643
|
-
|
|
1688
|
+
// Fail-closed: rewrite temp/upgrade roots and dual/wrong bins even without --force.
|
|
1689
|
+
const mustRewrite = hasTable && codexArkBlockNeedsRewrite(existing, absRoot);
|
|
1690
|
+
if (hasTable && !force && !mustRewrite) {
|
|
1644
1691
|
return { status: 'skipped', file };
|
|
1645
1692
|
}
|
|
1646
1693
|
let next;
|
|
@@ -1656,7 +1703,11 @@ args = [${argsToml}]`;
|
|
|
1656
1703
|
} catch (error) {
|
|
1657
1704
|
return { status: 'failed', file, message: error.message };
|
|
1658
1705
|
}
|
|
1659
|
-
return {
|
|
1706
|
+
return {
|
|
1707
|
+
status: hasTable ? 'updated' : 'written',
|
|
1708
|
+
file,
|
|
1709
|
+
...(mustRewrite && !force ? { reason: 'temp-or-stale-root' } : {}),
|
|
1710
|
+
};
|
|
1660
1711
|
}
|
|
1661
1712
|
|
|
1662
1713
|
// Detects stale/missing /ark-* skills in the Codex home prompts dir. Only nags
|
|
@@ -1735,10 +1786,385 @@ const COMMAND_GATE_TEXT_FILES = [
|
|
|
1735
1786
|
'.grok/hooks/ark-write-gate.json', '.grok/config.toml',
|
|
1736
1787
|
];
|
|
1737
1788
|
const COMMAND_GATE_JSON_FILES = ['.mcp.json', '.cursor/mcp.json'];
|
|
1789
|
+
// Primary CLI names (product) + one-major aliases. migrate-commands must strip ALL of these
|
|
1790
|
+
// before re-emitting a single preferred bin — otherwise a partial rename leaves
|
|
1791
|
+
// args: ["ark-mcp", "arkgate-mcp", ...] which breaks stdio MCP hosts.
|
|
1792
|
+
const ARK_MCP_BINS = new Set(['arkgate-mcp', 'ark-mcp']);
|
|
1793
|
+
const ARK_CHECK_BINS = new Set(['arkgate-check', 'ark-check']);
|
|
1794
|
+
const ARK_CLI_BINS = new Set(['arkgate', 'ark']);
|
|
1795
|
+
const PREFERRED_MCP_BIN = 'arkgate-mcp';
|
|
1796
|
+
const PREFERRED_CHECK_BIN = 'arkgate-check';
|
|
1797
|
+
const PREFERRED_CLI_BIN = 'arkgate';
|
|
1798
|
+
// Runner argv noise that is not a bin argument (pnpm exec form).
|
|
1799
|
+
const MCP_RUNNER_ARGV = new Set(['exec', '--config.verify-deps-before-run=false']);
|
|
1738
1800
|
// The runner token immediately before an ark command in a text command string.
|
|
1739
1801
|
// Matches npm/yarn runners and both pnpm forms (legacy `pnpm exec` + verify-deps-safe form).
|
|
1802
|
+
// Longer bin names first so `arkgate-check` is not partially matched as `ark`.
|
|
1740
1803
|
const RUNNER_BEFORE_ARK =
|
|
1741
|
-
/\b(?:npx|pnpm --config\.verify-deps-before-run=false exec|pnpm exec|yarn)(?= (?:ark-check|ark-mcp|ark)\b)/g;
|
|
1804
|
+
/\b(?:npx|pnpm --config\.verify-deps-before-run=false exec|pnpm exec|yarn)(?= (?:arkgate-check|arkgate-mcp|arkgate|ark-check|ark-mcp|ark)\b)/g;
|
|
1805
|
+
|
|
1806
|
+
/** Keep only MCP server flags from existing args (drop runner tokens + any ark* bin names). */
|
|
1807
|
+
function stripMcpServerArgs(args) {
|
|
1808
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
1809
|
+
return ['--root', '.', '--config', 'ark.config.json'];
|
|
1810
|
+
}
|
|
1811
|
+
const kept = args.filter(
|
|
1812
|
+
(entry) =>
|
|
1813
|
+
typeof entry === 'string' &&
|
|
1814
|
+
!MCP_RUNNER_ARGV.has(entry) &&
|
|
1815
|
+
!ARK_MCP_BINS.has(entry) &&
|
|
1816
|
+
!ARK_CHECK_BINS.has(entry) &&
|
|
1817
|
+
!ARK_CLI_BINS.has(entry)
|
|
1818
|
+
);
|
|
1819
|
+
return kept.length > 0 ? kept : ['--root', '.', '--config', 'ark.config.json'];
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
/** True when mcpServers.ark.args list more than one Ark MCP bin (broken dual rename). */
|
|
1823
|
+
function mcpArgsHaveDuplicateBins(args) {
|
|
1824
|
+
if (!Array.isArray(args)) return false;
|
|
1825
|
+
const hits = args.filter((entry) => ARK_MCP_BINS.has(entry));
|
|
1826
|
+
return hits.length > 1 || (hits.length === 1 && args.indexOf(hits[0]) !== args.lastIndexOf(hits[0]));
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function brokenMcpGateFiles(root) {
|
|
1830
|
+
const bad = [];
|
|
1831
|
+
for (const rel of COMMAND_GATE_JSON_FILES) {
|
|
1832
|
+
let json;
|
|
1833
|
+
try {
|
|
1834
|
+
json = JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8'));
|
|
1835
|
+
} catch {
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
const ark = json?.mcpServers?.ark;
|
|
1839
|
+
if (ark && mcpArgsHaveDuplicateBins(ark.args)) bad.push(rel);
|
|
1840
|
+
}
|
|
1841
|
+
return bad;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
/** Core layers whose optionality matters once they match files (presets share these names). */
|
|
1845
|
+
const CORE_LAYER_NAMES = new Set([
|
|
1846
|
+
'DomainModel',
|
|
1847
|
+
'ApplicationOrchestration',
|
|
1848
|
+
'PresentationAdapters',
|
|
1849
|
+
'PersistenceAdapters',
|
|
1850
|
+
]);
|
|
1851
|
+
|
|
1852
|
+
/** Temp / upgrade sandbox roots must never remain as Codex MCP --root. */
|
|
1853
|
+
function isTempOrUpgradeRoot(p) {
|
|
1854
|
+
if (!p || typeof p !== 'string') return false;
|
|
1855
|
+
const n = p.replace(/\\/g, '/');
|
|
1856
|
+
return (
|
|
1857
|
+
/\/var\/folders\//i.test(n) ||
|
|
1858
|
+
/\/tmp\//i.test(n) ||
|
|
1859
|
+
/\/Temp\//i.test(n) ||
|
|
1860
|
+
/ark-upgrade/i.test(n) ||
|
|
1861
|
+
/\/T\/(?:ark-|grok-)/i.test(n) ||
|
|
1862
|
+
/[\\/]AppData[\\/]Local[\\/]Temp[\\/]/i.test(n)
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
/** Extract --root value from Codex [mcp_servers.ark] args array text. */
|
|
1867
|
+
function extractCodexArkRootFromToml(tomlText) {
|
|
1868
|
+
if (!tomlText || typeof tomlText !== 'string') return null;
|
|
1869
|
+
const start = tomlText.search(/(^|\n)\[mcp_servers\.ark\]/);
|
|
1870
|
+
if (start < 0) return null;
|
|
1871
|
+
const rest = tomlText.slice(start);
|
|
1872
|
+
const endMatch = rest.slice(1).search(/\n\[/);
|
|
1873
|
+
const block = endMatch >= 0 ? rest.slice(0, endMatch + 1) : rest;
|
|
1874
|
+
// args = ["arkgate-mcp", "--root", "/abs/path", ...]
|
|
1875
|
+
const rootIdx = block.search(/"--root"\s*,\s*"/);
|
|
1876
|
+
if (rootIdx < 0) {
|
|
1877
|
+
// alternate: --root as adjacent string after any bin
|
|
1878
|
+
const m = block.match(/"--root"\s*,\s*"([^"]+)"/);
|
|
1879
|
+
return m ? m[1] : null;
|
|
1880
|
+
}
|
|
1881
|
+
const m = block.slice(rootIdx).match(/"--root"\s*,\s*"([^"]+)"/);
|
|
1882
|
+
return m ? m[1] : null;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function codexArkBlockHasPreferredBin(tomlText) {
|
|
1886
|
+
if (!tomlText) return false;
|
|
1887
|
+
const start = tomlText.search(/(^|\n)\[mcp_servers\.ark\]/);
|
|
1888
|
+
if (start < 0) return false;
|
|
1889
|
+
const rest = tomlText.slice(start);
|
|
1890
|
+
const endMatch = rest.slice(1).search(/\n\[/);
|
|
1891
|
+
const block = endMatch >= 0 ? rest.slice(0, endMatch + 1) : rest;
|
|
1892
|
+
const bins = [...block.matchAll(/"(arkgate-mcp|ark-mcp)"/g)].map((m) => m[1]);
|
|
1893
|
+
if (bins.length > 1) return false;
|
|
1894
|
+
return bins.length === 1 && bins[0] === PREFERRED_MCP_BIN;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function codexArkBlockNeedsRewrite(tomlText, absRoot) {
|
|
1898
|
+
if (!tomlText || !tomlText.includes('[mcp_servers.ark]')) return true;
|
|
1899
|
+
const rootArg = extractCodexArkRootFromToml(tomlText);
|
|
1900
|
+
if (!rootArg || isTempOrUpgradeRoot(rootArg)) return true;
|
|
1901
|
+
try {
|
|
1902
|
+
if (path.resolve(rootArg) !== path.resolve(absRoot)) return true;
|
|
1903
|
+
} catch {
|
|
1904
|
+
return true;
|
|
1905
|
+
}
|
|
1906
|
+
if (!codexArkBlockHasPreferredBin(tomlText)) return true;
|
|
1907
|
+
return false;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
/**
|
|
1911
|
+
* Adoption completeness (separate from 0–100 fitness). Pure-ish: filesystem + config.
|
|
1912
|
+
* @returns {{ gaps: object[], hosts: object[], mcp: object, codexHome: object|null, coreOptional: object[], originReport: object, baseline: object, layerBalance: object|null }}
|
|
1913
|
+
*/
|
|
1914
|
+
function collectAdoptionGaps(root, config, coverage) {
|
|
1915
|
+
const gaps = [];
|
|
1916
|
+
const adopted = fs.existsSync(path.join(root, 'AGENTS.md'));
|
|
1917
|
+
const isProducer = fs.existsSync(path.join(root, 'templates', 'skills'));
|
|
1918
|
+
|
|
1919
|
+
// --- Repo MCP dual-bin ---
|
|
1920
|
+
const dualMcp = brokenMcpGateFiles(root);
|
|
1921
|
+
const mcp = {
|
|
1922
|
+
dualBinFiles: dualMcp,
|
|
1923
|
+
ok: dualMcp.length === 0,
|
|
1924
|
+
};
|
|
1925
|
+
if (dualMcp.length > 0) {
|
|
1926
|
+
gaps.push({
|
|
1927
|
+
id: 'mcp-dual-bin',
|
|
1928
|
+
severity: 'warn',
|
|
1929
|
+
message: `Broken MCP argv in ${dualMcp.join(', ')}: more than one of ark-mcp/arkgate-mcp`,
|
|
1930
|
+
fix: arkCommand(root, 'ark-check', '--install-agent-gates --migrate-commands'),
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// --- Host completeness (only when project already adopted gates) ---
|
|
1935
|
+
const hosts = [];
|
|
1936
|
+
if (adopted && !isProducer) {
|
|
1937
|
+
const skillNames = skillTemplateNames();
|
|
1938
|
+
const hostChecks = [
|
|
1939
|
+
{
|
|
1940
|
+
host: 'grok',
|
|
1941
|
+
dir: '.grok',
|
|
1942
|
+
skill: (n) => path.join(root, '.grok', 'skills', n, 'SKILL.md'),
|
|
1943
|
+
extras: [
|
|
1944
|
+
['.grok/hooks/ark-write-gate.json', 'write-gate hook'],
|
|
1945
|
+
['.grok/config.toml', 'project MCP config'],
|
|
1946
|
+
],
|
|
1947
|
+
toolsFlag: 'grok',
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
host: 'claude',
|
|
1951
|
+
dir: '.claude',
|
|
1952
|
+
skill: (n) => path.join(root, '.claude', 'skills', n, 'SKILL.md'),
|
|
1953
|
+
extras: [['.claude/settings.json', 'settings/hooks']],
|
|
1954
|
+
toolsFlag: 'claude',
|
|
1955
|
+
},
|
|
1956
|
+
{
|
|
1957
|
+
host: 'cursor',
|
|
1958
|
+
dir: '.cursor',
|
|
1959
|
+
skill: (n) => path.join(root, '.cursor', 'commands', `${n}.md`),
|
|
1960
|
+
extras: [['.cursor/mcp.json', 'MCP config']],
|
|
1961
|
+
toolsFlag: 'cursor',
|
|
1962
|
+
},
|
|
1963
|
+
];
|
|
1964
|
+
for (const h of hostChecks) {
|
|
1965
|
+
if (!fs.existsSync(path.join(root, h.dir))) continue;
|
|
1966
|
+
const missingSkills = skillNames.filter((n) => !fs.existsSync(h.skill(n)));
|
|
1967
|
+
const missingExtras = h.extras.filter(([rel]) => !fs.existsSync(path.join(root, rel)));
|
|
1968
|
+
const complete = missingSkills.length === 0 && missingExtras.length === 0;
|
|
1969
|
+
hosts.push({
|
|
1970
|
+
host: h.host,
|
|
1971
|
+
present: true,
|
|
1972
|
+
complete,
|
|
1973
|
+
missingSkills: missingSkills.length,
|
|
1974
|
+
missingExtras: missingExtras.map(([, label]) => label),
|
|
1975
|
+
});
|
|
1976
|
+
if (!complete) {
|
|
1977
|
+
gaps.push({
|
|
1978
|
+
id: `host-${h.host}-incomplete`,
|
|
1979
|
+
severity: 'warn',
|
|
1980
|
+
message: `${h.host} dir present but incomplete (${missingSkills.length} skill(s) missing${
|
|
1981
|
+
missingExtras.length ? `; missing ${missingExtras.map(([, l]) => l).join(', ')}` : ''
|
|
1982
|
+
})`,
|
|
1983
|
+
fix: arkCommand(
|
|
1984
|
+
root,
|
|
1985
|
+
'ark-check',
|
|
1986
|
+
`--install-agent-gates --tools ${h.toolsFlag} --force`
|
|
1987
|
+
),
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// --- Codex home MCP (temp path / wrong root / dual bin) ---
|
|
1994
|
+
let codexHome = null;
|
|
1995
|
+
if (adopted && !isProducer) {
|
|
1996
|
+
const codexFile = codexConfigPath();
|
|
1997
|
+
let toml = '';
|
|
1998
|
+
try {
|
|
1999
|
+
if (fs.existsSync(codexFile)) toml = fs.readFileSync(codexFile, 'utf8');
|
|
2000
|
+
} catch {
|
|
2001
|
+
toml = '';
|
|
2002
|
+
}
|
|
2003
|
+
if (toml.includes('[mcp_servers.ark]')) {
|
|
2004
|
+
const rootArg = extractCodexArkRootFromToml(toml);
|
|
2005
|
+
const absRoot = path.resolve(root);
|
|
2006
|
+
const temp = isTempOrUpgradeRoot(rootArg);
|
|
2007
|
+
let wrongRoot = false;
|
|
2008
|
+
try {
|
|
2009
|
+
wrongRoot = rootArg ? path.resolve(rootArg) !== absRoot : true;
|
|
2010
|
+
} catch {
|
|
2011
|
+
wrongRoot = true;
|
|
2012
|
+
}
|
|
2013
|
+
const preferredBin = codexArkBlockHasPreferredBin(toml);
|
|
2014
|
+
const needsRewrite = codexArkBlockNeedsRewrite(toml, absRoot);
|
|
2015
|
+
codexHome = {
|
|
2016
|
+
file: codexFile,
|
|
2017
|
+
root: rootArg,
|
|
2018
|
+
tempPath: temp,
|
|
2019
|
+
wrongRoot,
|
|
2020
|
+
preferredBin,
|
|
2021
|
+
needsRewrite,
|
|
2022
|
+
};
|
|
2023
|
+
if (needsRewrite) {
|
|
2024
|
+
gaps.push({
|
|
2025
|
+
id: 'codex-home-mcp',
|
|
2026
|
+
severity: temp || wrongRoot ? 'warn' : 'info',
|
|
2027
|
+
message: temp
|
|
2028
|
+
? `Codex home MCP --root points at a temp/upgrade path (${rootArg})`
|
|
2029
|
+
: wrongRoot
|
|
2030
|
+
? `Codex home MCP --root is not this project (${rootArg || 'missing'} ≠ ${absRoot})`
|
|
2031
|
+
: `Codex home MCP should use a single ${PREFERRED_MCP_BIN} bin with absolute project paths`,
|
|
2032
|
+
fix: arkCommand(
|
|
2033
|
+
root,
|
|
2034
|
+
'ark-check',
|
|
2035
|
+
'--install-agent-gates --codex-home --force'
|
|
2036
|
+
),
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// --- Core layers optional but populated ---
|
|
2043
|
+
const coreOptional = [];
|
|
2044
|
+
const layerRows = coverage?.layers ?? [];
|
|
2045
|
+
const countByName = new Map(layerRows.map((r) => [r.name, r.files]));
|
|
2046
|
+
for (const layer of config?.layers ?? []) {
|
|
2047
|
+
if (!CORE_LAYER_NAMES.has(layer.name)) continue;
|
|
2048
|
+
if (layer.optional !== true) continue;
|
|
2049
|
+
const files = countByName.get(layer.name) ?? 0;
|
|
2050
|
+
if (files > 0) {
|
|
2051
|
+
coreOptional.push({ layer: layer.name, files });
|
|
2052
|
+
gaps.push({
|
|
2053
|
+
id: `core-optional-${layer.name}`,
|
|
2054
|
+
severity: 'info',
|
|
2055
|
+
message: `Core layer ${layer.name} has ${files} file(s) but is still optional: true — contract is weaker than the tree`,
|
|
2056
|
+
fix: `Edit ark.config.json: remove optional on ${layer.name} (or set false), then ${arkCommand(root, 'ark-check', '--strict-config')}`,
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// --- Origin report ---
|
|
2062
|
+
const originJson = path.join(root, '.ark', 'reports', 'origin.json');
|
|
2063
|
+
const originReport = {
|
|
2064
|
+
present: fs.existsSync(originJson),
|
|
2065
|
+
path: '.ark/reports/origin.json',
|
|
2066
|
+
};
|
|
2067
|
+
if (adopted && !originReport.present && (coverage?.governed?.percent ?? 0) >= 50) {
|
|
2068
|
+
gaps.push({
|
|
2069
|
+
id: 'origin-report-missing',
|
|
2070
|
+
severity: 'info',
|
|
2071
|
+
message: 'No origin architecture snapshot under .ark/reports/ yet',
|
|
2072
|
+
fix: arkCommand(root, 'ark-check', '--report ark-report.html'),
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// --- Baseline policy ---
|
|
2077
|
+
const baselinePath = path.join(root, '.ark-baseline.json');
|
|
2078
|
+
const baselineExists = fs.existsSync(baselinePath);
|
|
2079
|
+
let frozenKeys = 0;
|
|
2080
|
+
if (baselineExists) {
|
|
2081
|
+
try {
|
|
2082
|
+
const raw = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
|
|
2083
|
+
frozenKeys = Array.isArray(raw.violations) ? raw.violations.length : 0;
|
|
2084
|
+
} catch {
|
|
2085
|
+
frozenKeys = 0;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
let primaryPathUsesBaseline = false;
|
|
2089
|
+
try {
|
|
2090
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
2091
|
+
const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
2092
|
+
primaryPathUsesBaseline = Object.values(scripts).some(
|
|
2093
|
+
(s) => typeof s === 'string' && s.includes('--baseline')
|
|
2094
|
+
);
|
|
2095
|
+
} catch {
|
|
2096
|
+
/* no package.json */
|
|
2097
|
+
}
|
|
2098
|
+
if (!primaryPathUsesBaseline) {
|
|
2099
|
+
try {
|
|
2100
|
+
const wfDir = path.join(root, '.github', 'workflows');
|
|
2101
|
+
if (fs.existsSync(wfDir)) {
|
|
2102
|
+
for (const f of fs.readdirSync(wfDir)) {
|
|
2103
|
+
if (!/\.ya?ml$/i.test(f)) continue;
|
|
2104
|
+
const text = fs.readFileSync(path.join(wfDir, f), 'utf8');
|
|
2105
|
+
if (text.includes('--baseline') && (text.includes('ark-check') || text.includes('arkgate-check'))) {
|
|
2106
|
+
primaryPathUsesBaseline = true;
|
|
2107
|
+
break;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
} catch {
|
|
2112
|
+
/* ignore */
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
const baseline = {
|
|
2116
|
+
exists: baselineExists,
|
|
2117
|
+
frozenKeys,
|
|
2118
|
+
primaryPathUsesBaseline,
|
|
2119
|
+
signal: baselineExists
|
|
2120
|
+
? frozenKeys === 0
|
|
2121
|
+
? 'keep-empty'
|
|
2122
|
+
: 'active-ratchet'
|
|
2123
|
+
: 'absent',
|
|
2124
|
+
};
|
|
2125
|
+
if (adopted && baselineExists && frozenKeys === 0 && !primaryPathUsesBaseline) {
|
|
2126
|
+
gaps.push({
|
|
2127
|
+
id: 'baseline-unused',
|
|
2128
|
+
severity: 'info',
|
|
2129
|
+
message:
|
|
2130
|
+
'Empty .ark-baseline.json exists but primary scripts/CI do not pass --baseline (policy unclear)',
|
|
2131
|
+
fix: 'Either add --baseline .ark-baseline.json to check:architecture / CI, or remove the unused baseline file',
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// --- Educational layer balance (not a violation) ---
|
|
2136
|
+
let layerBalance = null;
|
|
2137
|
+
const total = layerRows.reduce((s, r) => s + (r.files || 0), 0);
|
|
2138
|
+
if (total >= 20) {
|
|
2139
|
+
const presentation = layerRows.find((r) => r.name === 'PresentationAdapters');
|
|
2140
|
+
const domain = layerRows.find((r) => r.name === 'DomainModel');
|
|
2141
|
+
if (presentation && domain) {
|
|
2142
|
+
const pShare = presentation.files / total;
|
|
2143
|
+
const dShare = domain.files / total;
|
|
2144
|
+
if (pShare >= 0.5 && dShare < 0.1) {
|
|
2145
|
+
layerBalance = {
|
|
2146
|
+
kind: 'presentation-heavy-thin-domain',
|
|
2147
|
+
presentationFiles: presentation.files,
|
|
2148
|
+
domainFiles: domain.files,
|
|
2149
|
+
totalFiles: total,
|
|
2150
|
+
educational:
|
|
2151
|
+
'Presentation holds most of the tree while DomainModel is thin — common for UI apps; consider extracting domain types/use-cases as the product grows. Educational only (not a gate failure).',
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
return {
|
|
2158
|
+
gaps,
|
|
2159
|
+
hosts,
|
|
2160
|
+
mcp,
|
|
2161
|
+
codexHome,
|
|
2162
|
+
coreOptional,
|
|
2163
|
+
originReport,
|
|
2164
|
+
baseline,
|
|
2165
|
+
layerBalance,
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
1742
2168
|
|
|
1743
2169
|
// Gate files whose Ark command runner doesn't match this project's package manager — the
|
|
1744
2170
|
// advisory (and --migrate-commands) target. Returns [] for npm/unknown projects (npx is right)
|
|
@@ -1800,6 +2226,8 @@ function warnLockfileConflict(root) {
|
|
|
1800
2226
|
// --migrate-commands: rewrite ONLY the Ark command runner in existing gate files to the
|
|
1801
2227
|
// project's package manager (no --force clobber). Closes the upgrade gap where a repo that
|
|
1802
2228
|
// adopted before the package-manager-aware templates keeps a stale `npx`.
|
|
2229
|
+
// Also normalizes MCP JSON to a single preferred bin (arkgate-mcp), stripping any dual
|
|
2230
|
+
// ark-mcp + arkgate-mcp residue left by partial renames during package identity cutover.
|
|
1803
2231
|
function runMigrateCommands(root) {
|
|
1804
2232
|
const runner = execRunner(root);
|
|
1805
2233
|
const changed = [];
|
|
@@ -1811,7 +2239,12 @@ function runMigrateCommands(root) {
|
|
|
1811
2239
|
} catch {
|
|
1812
2240
|
continue;
|
|
1813
2241
|
}
|
|
1814
|
-
|
|
2242
|
+
let next = text.replace(RUNNER_BEFORE_ARK, runner);
|
|
2243
|
+
// Prefer primary product bins in command strings (aliases still work if left alone).
|
|
2244
|
+
next = next
|
|
2245
|
+
.replace(/\bark-mcp\b/g, PREFERRED_MCP_BIN)
|
|
2246
|
+
.replace(/\bark-check\b/g, PREFERRED_CHECK_BIN);
|
|
2247
|
+
// Do not blanket-replace bare `ark` — it appears in prose ("Ark check", product name).
|
|
1815
2248
|
if (next !== text) {
|
|
1816
2249
|
fs.writeFileSync(full, next);
|
|
1817
2250
|
changed.push(rel);
|
|
@@ -1827,23 +2260,23 @@ function runMigrateCommands(root) {
|
|
|
1827
2260
|
}
|
|
1828
2261
|
const ark = json?.mcpServers?.ark;
|
|
1829
2262
|
if (!ark) continue;
|
|
1830
|
-
const binArgs =
|
|
1831
|
-
|
|
1832
|
-
: ['--root', '.', '--config', 'ark.config.json'];
|
|
1833
|
-
const parts = execCommandParts(root, 'ark-mcp', binArgs);
|
|
2263
|
+
const binArgs = stripMcpServerArgs(ark.args);
|
|
2264
|
+
const parts = execCommandParts(root, PREFERRED_MCP_BIN, binArgs);
|
|
1834
2265
|
if (ark.command !== parts.command || JSON.stringify(ark.args) !== JSON.stringify(parts.args)) {
|
|
1835
2266
|
json.mcpServers.ark = { ...ark, ...parts };
|
|
1836
2267
|
fs.writeFileSync(full, `${JSON.stringify(json, null, 2)}\n`);
|
|
1837
2268
|
changed.push(rel);
|
|
1838
2269
|
}
|
|
1839
2270
|
}
|
|
1840
|
-
const pm = runner === 'pnpm exec' ? 'pnpm' : runner;
|
|
1841
|
-
console.log(`Migrated
|
|
2271
|
+
const pm = runner === 'pnpm exec' || runner.startsWith('pnpm ') ? 'pnpm' : runner;
|
|
2272
|
+
console.log(`Migrated ArkGate command runners to "${pm}" and normalized MCP bins in gate files.`);
|
|
1842
2273
|
if (changed.length === 0) {
|
|
1843
|
-
console.log(' Nothing to change —
|
|
2274
|
+
console.log(' Nothing to change — runners and MCP bins already look correct.');
|
|
1844
2275
|
} else {
|
|
1845
2276
|
for (const rel of changed) console.log(` updated ${rel}`);
|
|
1846
|
-
console.log(
|
|
2277
|
+
console.log(
|
|
2278
|
+
` (runner + single MCP bin \`${PREFERRED_MCP_BIN}\`; customized non-command content is untouched.)`
|
|
2279
|
+
);
|
|
1847
2280
|
}
|
|
1848
2281
|
warnLockfileConflict(root);
|
|
1849
2282
|
}
|
|
@@ -2970,8 +3403,12 @@ function detectEnforcement(root) {
|
|
|
2970
3403
|
(f) => has(f) && (fileIncludes(f, 'arkgate') || fileIncludes(f, 'ark-runtime-kernel'))
|
|
2971
3404
|
);
|
|
2972
3405
|
const writeGateFile =
|
|
2973
|
-
(fileIncludes('.claude/settings.json', '
|
|
3406
|
+
((fileIncludes('.claude/settings.json', 'arkgate-mcp') ||
|
|
3407
|
+
fileIncludes('.claude/settings.json', 'ark-mcp')) &&
|
|
3408
|
+
'.claude/settings.json') ||
|
|
2974
3409
|
(has('.cursor/mcp.json') && '.cursor/mcp.json') ||
|
|
3410
|
+
(fileIncludes('.grok/hooks/ark-write-gate.json', 'arkgate-mcp') &&
|
|
3411
|
+
'.grok/hooks/ark-write-gate.json') ||
|
|
2975
3412
|
null;
|
|
2976
3413
|
return [
|
|
2977
3414
|
{ name: 'Write gate', where: writeGateFile, what: 'blocks a bad edit as you type (PreToolUse hook / MCP)' },
|
|
@@ -3311,6 +3748,7 @@ function renderHtmlReport({
|
|
|
3311
3748
|
originSnapshot = null,
|
|
3312
3749
|
currentSnapshot = null,
|
|
3313
3750
|
originJustCreated = false,
|
|
3751
|
+
adoption = null,
|
|
3314
3752
|
}) {
|
|
3315
3753
|
const layers = Array.isArray(config.layers) ? config.layers : [];
|
|
3316
3754
|
const rules = Array.isArray(config.rules) ? config.rules : [];
|
|
@@ -3362,6 +3800,8 @@ function renderHtmlReport({
|
|
|
3362
3800
|
scoreCaption,
|
|
3363
3801
|
} = fitness;
|
|
3364
3802
|
|
|
3803
|
+
const adoptionView = adoption || collectAdoptionGaps(root, config, coverage);
|
|
3804
|
+
|
|
3365
3805
|
// ── Senior diagnostics (coupling, purity, contract density) ──────────────
|
|
3366
3806
|
const layerNames = ordered.map((l) => l.name);
|
|
3367
3807
|
const pairCount = Math.max(1, layers.length * Math.max(0, layers.length - 1));
|
|
@@ -3867,6 +4307,45 @@ function renderHtmlReport({
|
|
|
3867
4307
|
</div>
|
|
3868
4308
|
</div>
|
|
3869
4309
|
|
|
4310
|
+
<div class="section card" id="adoption">
|
|
4311
|
+
<h2>Adoption</h2>
|
|
4312
|
+
<p class="dim" style="margin:.15rem 0 .75rem;font-size:.88rem">
|
|
4313
|
+
Co-pilot completeness — separate from the 0–100 fitness score above. Hosts, MCP health, origin snapshot, core optionality, baseline policy.
|
|
4314
|
+
</p>
|
|
4315
|
+
<div class="kpis" style="margin-bottom:.75rem">
|
|
4316
|
+
<div class="kpi"><b>${adoptionView.gaps.length === 0 ? 'OK' : adoptionView.gaps.length}</b><span>${adoptionView.gaps.length === 0 ? 'No adoption gaps' : 'Adoption gap(s)'}</span></div>
|
|
4317
|
+
<div class="kpi"><b>${adoptionView.originReport.present ? 'yes' : 'no'}</b><span>Origin report</span></div>
|
|
4318
|
+
<div class="kpi"><b>${esc(adoptionView.baseline.signal)}</b><span>Baseline policy</span></div>
|
|
4319
|
+
<div class="kpi"><b>${adoptionView.mcp.ok ? 'ok' : 'fix'}</b><span>Repo MCP argv</span></div>
|
|
4320
|
+
</div>
|
|
4321
|
+
${
|
|
4322
|
+
adoptionView.gaps.length
|
|
4323
|
+
? `<ul class="senior-list">${adoptionView.gaps
|
|
4324
|
+
.map(
|
|
4325
|
+
(g) =>
|
|
4326
|
+
`<li><b>${esc(g.id)}</b> — ${esc(g.message)}${
|
|
4327
|
+
g.fix ? `<br/><code>${esc(g.fix)}</code>` : ''
|
|
4328
|
+
}</li>`
|
|
4329
|
+
)
|
|
4330
|
+
.join('')}</ul>`
|
|
4331
|
+
: '<p class="clean-body">No adoption gaps detected for hosts, MCP, core optionality, or origin.</p>'
|
|
4332
|
+
}
|
|
4333
|
+
${
|
|
4334
|
+
adoptionView.coreOptional.length
|
|
4335
|
+
? `<p class="dim" style="margin-top:.65rem">Optional-but-populated cores: <code>${adoptionView.coreOptional
|
|
4336
|
+
.map((c) => `${esc(c.layer)} (${c.files})`)
|
|
4337
|
+
.join('</code>, <code>')}</code></p>`
|
|
4338
|
+
: ''
|
|
4339
|
+
}
|
|
4340
|
+
${
|
|
4341
|
+
adoptionView.hosts.length
|
|
4342
|
+
? `<p class="dim" style="margin-top:.4rem">Hosts: ${adoptionView.hosts
|
|
4343
|
+
.map((h) => `${esc(h.host)}${h.complete ? ' ✓' : ' incomplete'}`)
|
|
4344
|
+
.join(' · ')}</p>`
|
|
4345
|
+
: ''
|
|
4346
|
+
}
|
|
4347
|
+
</div>
|
|
4348
|
+
|
|
3870
4349
|
<div class="section grid-2">
|
|
3871
4350
|
<div class="card">
|
|
3872
4351
|
<h2>Architecture map</h2>
|
|
@@ -4102,6 +4581,14 @@ function renderHtmlReport({
|
|
|
4102
4581
|
: '<p class="dim">No <code>intentPrefixes</code> on layers — runtime intent governance and string-intent checks have less to bind to.</p>'
|
|
4103
4582
|
}
|
|
4104
4583
|
|
|
4584
|
+
<h3>Layer balance (educational)</h3>
|
|
4585
|
+
${
|
|
4586
|
+
adoptionView.layerBalance
|
|
4587
|
+
? `<p class="dim" style="margin:.1rem 0 .55rem;font-size:.88rem">${esc(adoptionView.layerBalance.educational)}</p>
|
|
4588
|
+
<p class="meta">PresentationAdapters ${adoptionView.layerBalance.presentationFiles} · DomainModel ${adoptionView.layerBalance.domainFiles} · total ${adoptionView.layerBalance.totalFiles}</p>`
|
|
4589
|
+
: '<p class="dim" style="margin:.1rem 0 .55rem;font-size:.88rem">No presentation-heavy / thin-domain imbalance flagged (educational only when Presentation ≥50% and Domain <10% of files).</p>'
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4105
4592
|
<h3>Pattern forensics</h3>
|
|
4106
4593
|
<div class="grid-2" style="margin-top:.45rem">
|
|
4107
4594
|
<div>
|
|
@@ -4431,6 +4918,7 @@ function runDoctor(root, config, files, rules, violations, asJson, options = {})
|
|
|
4431
4918
|
const gatesMissing = missingGates(root);
|
|
4432
4919
|
const skillGaps = detectSkillGaps(root);
|
|
4433
4920
|
const staleRunners = staleRunnerGateFiles(root);
|
|
4921
|
+
const adoption = collectAdoptionGaps(root, config, cov);
|
|
4434
4922
|
const baseline = readBaseline(root, '.ark-baseline.json');
|
|
4435
4923
|
const currentKeys = new Set(violations.map(baselineKey));
|
|
4436
4924
|
const suppressed = baseline.exists
|
|
@@ -4472,10 +4960,12 @@ function runDoctor(root, config, files, rules, violations, asJson, options = {})
|
|
|
4472
4960
|
exists: baseline.exists,
|
|
4473
4961
|
frozen: baseline.exists ? baseline.keys.size : 0,
|
|
4474
4962
|
stale: staleBaseline,
|
|
4963
|
+
policy: adoption.baseline,
|
|
4475
4964
|
},
|
|
4476
4965
|
gatesMissing,
|
|
4477
4966
|
skillGaps,
|
|
4478
4967
|
staleRunnerFiles: staleRunners,
|
|
4968
|
+
adoption,
|
|
4479
4969
|
newHere: showNewHere
|
|
4480
4970
|
? {
|
|
4481
4971
|
show: true,
|
|
@@ -4617,6 +5107,37 @@ function runDoctor(root, config, files, rules, violations, asJson, options = {})
|
|
|
4617
5107
|
actions.push(`migrate command runners (${arkCommand(root, 'ark-check', '--install-agent-gates --migrate-commands')})`);
|
|
4618
5108
|
}
|
|
4619
5109
|
|
|
5110
|
+
// Adoption completeness (hosts, MCP health, codex home, core optionality, origin, baseline policy)
|
|
5111
|
+
console.log('');
|
|
5112
|
+
console.log(color.bold('Adoption (separate from fitness score)'));
|
|
5113
|
+
if (adoption.gaps.length === 0 && !adoption.layerBalance) {
|
|
5114
|
+
line(ok, 'Hosts, MCP argv, core optionality, origin report, and baseline policy look complete');
|
|
5115
|
+
} else {
|
|
5116
|
+
for (const gap of adoption.gaps) {
|
|
5117
|
+
const mark = gap.severity === 'warn' ? warn : gap.severity === 'info' ? warn : bad;
|
|
5118
|
+
line(mark, gap.message);
|
|
5119
|
+
if (gap.fix) line(' ', color.dim(`Fix: ${gap.fix}`));
|
|
5120
|
+
actions.push(gap.fix || gap.message);
|
|
5121
|
+
}
|
|
5122
|
+
if (adoption.layerBalance) {
|
|
5123
|
+
line(warn, color.dim(adoption.layerBalance.educational));
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
if (adoption.baseline) {
|
|
5127
|
+
line(
|
|
5128
|
+
' ',
|
|
5129
|
+
color.dim(
|
|
5130
|
+
`Baseline policy: ${adoption.baseline.signal}` +
|
|
5131
|
+
(adoption.baseline.primaryPathUsesBaseline
|
|
5132
|
+
? ' · primary path uses --baseline'
|
|
5133
|
+
: ' · primary path does not use --baseline')
|
|
5134
|
+
)
|
|
5135
|
+
);
|
|
5136
|
+
}
|
|
5137
|
+
if (adoption.originReport.present) {
|
|
5138
|
+
line(ok, 'Origin architecture snapshot present (.ark/reports/origin.json)');
|
|
5139
|
+
}
|
|
5140
|
+
|
|
4620
5141
|
console.log('');
|
|
4621
5142
|
if (actions.length === 0) {
|
|
4622
5143
|
console.log(color.green('✔ Healthy — nothing to do.'));
|
|
@@ -4744,24 +5265,39 @@ async function main() {
|
|
|
4744
5265
|
|
|
4745
5266
|
// Resolve TypeScript from the project first, then Ark's own install, then bare import.
|
|
4746
5267
|
// --plan can still run honestly (coverage + empty violations) when TS is missing.
|
|
4747
|
-
|
|
4748
|
-
|
|
5268
|
+
// Early TypeScript 7 native builds may load but lack a JS `sys` host — we fall back.
|
|
5269
|
+
const loaded = await loadTypeScript(root);
|
|
5270
|
+
if (!loaded?.ts) {
|
|
4749
5271
|
if (args.plan) {
|
|
4750
5272
|
const cov = computeCoverage(root, config, files, rules);
|
|
4751
5273
|
if (!args.json) {
|
|
4752
5274
|
console.log(
|
|
4753
5275
|
color.yellow(
|
|
4754
|
-
`TypeScript not found — plan shows coverage honesty only (no import graph). Install with: ${installDevHint(root, 'typescript')}`
|
|
5276
|
+
`TypeScript not found — plan shows coverage honesty only (no import graph). Install with: ${installDevHint(root, 'typescript')} (supported: 5.x–7.x; see docs/typescript-support.md)`
|
|
4755
5277
|
)
|
|
4756
5278
|
);
|
|
4757
5279
|
}
|
|
4758
5280
|
runPlan(root, [], args.json, cov.governed.percent, cov.governed.totalFiles);
|
|
4759
5281
|
return;
|
|
4760
5282
|
}
|
|
4761
|
-
console.error(
|
|
5283
|
+
console.error(
|
|
5284
|
+
`ark-check requires a JS-API TypeScript (5.x–7.x with ts.sys). Install with: ${installDevHint(root, 'typescript')} — see docs/typescript-support.md`
|
|
5285
|
+
);
|
|
4762
5286
|
process.exitCode = 2;
|
|
4763
5287
|
return;
|
|
4764
5288
|
}
|
|
5289
|
+
const { ts } = loaded;
|
|
5290
|
+
if (loaded.fallbackReason && !args.json) {
|
|
5291
|
+
console.log(color.yellow(loaded.fallbackReason));
|
|
5292
|
+
}
|
|
5293
|
+
if (process.env.ARK_DEBUG_TS === '1' && !args.json) {
|
|
5294
|
+
console.log(
|
|
5295
|
+
color.dim(
|
|
5296
|
+
`[ark-check] TypeScript ${loaded.version ?? '?'} via ${loaded.source}` +
|
|
5297
|
+
(loaded.fallbackReason ? ' (fallback)' : '')
|
|
5298
|
+
)
|
|
5299
|
+
);
|
|
5300
|
+
}
|
|
4765
5301
|
|
|
4766
5302
|
const manifestIntentLayers = intentLayersFromManifest(manifest);
|
|
4767
5303
|
const compilerOptionsFor = createCompilerOptionsLookup(ts, root, args.tsconfig);
|
|
@@ -5218,6 +5754,16 @@ async function main() {
|
|
|
5218
5754
|
);
|
|
5219
5755
|
}
|
|
5220
5756
|
|
|
5757
|
+
const brokenMcp = brokenMcpGateFiles(root);
|
|
5758
|
+
if (brokenMcp.length > 0) {
|
|
5759
|
+
console.log(
|
|
5760
|
+
color.yellow(
|
|
5761
|
+
`Broken MCP argv in ${brokenMcp.join(', ')}: more than one of ark-mcp/arkgate-mcp in args ` +
|
|
5762
|
+
`(stdio hosts get a double binary name). Fix: ${arkCommand(root, 'ark-check', '--install-agent-gates --migrate-commands')}`
|
|
5763
|
+
)
|
|
5764
|
+
);
|
|
5765
|
+
}
|
|
5766
|
+
|
|
5221
5767
|
if (codexHomeGap) {
|
|
5222
5768
|
const parts = [];
|
|
5223
5769
|
if (codexHomeGap.missing > 0) parts.push(`${codexHomeGap.missing} missing`);
|