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/ark-check.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
loadTypeScript,
|
|
34
34
|
detectSkillGaps,
|
|
35
35
|
detectCodexHomeGap,
|
|
36
|
+
detectActiveAgentHost,
|
|
36
37
|
missingGates,
|
|
37
38
|
staleRunnerGateFiles,
|
|
38
39
|
brokenMcpGateFiles,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
checkArchitectureScriptSnippet,
|
|
42
43
|
arkCheckCommand,
|
|
43
44
|
arkPackageVersion,
|
|
45
|
+
compactRouterHost,
|
|
44
46
|
REQUIRED_GATE_FILES,
|
|
45
47
|
codexPromptsDir,
|
|
46
48
|
detectWritePathCapabilities,
|
|
@@ -56,6 +58,7 @@ import {
|
|
|
56
58
|
reportsDir,
|
|
57
59
|
readJsonSafe,
|
|
58
60
|
} from './lib/html-report.mjs';
|
|
61
|
+
import { shouldOpenHtmlReport, openHtmlInBrowser } from './lib/open-html.mjs';
|
|
59
62
|
import {
|
|
60
63
|
computeCoverage,
|
|
61
64
|
runCoverage,
|
|
@@ -81,6 +84,8 @@ import {
|
|
|
81
84
|
import {
|
|
82
85
|
ARCHITECTURE_PRESETS,
|
|
83
86
|
} from './lib/presets.mjs';
|
|
87
|
+
import { loadArkConfigContract, parseArkConfigJson } from './lib/config-contract.mjs';
|
|
88
|
+
import { createAdapterResult } from './lib/adapter-contract.mjs';
|
|
84
89
|
|
|
85
90
|
import {
|
|
86
91
|
collectGovernedFiles,
|
|
@@ -91,6 +96,7 @@ import {
|
|
|
91
96
|
configWarning,
|
|
92
97
|
} from './lib/config-warnings.mjs';
|
|
93
98
|
import { runArchitectureScan } from './lib/architecture-scan.mjs';
|
|
99
|
+
import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
|
|
94
100
|
|
|
95
101
|
|
|
96
102
|
function parseArgs(argv) {
|
|
@@ -101,11 +107,12 @@ function parseArgs(argv) {
|
|
|
101
107
|
printConfig: undefined,
|
|
102
108
|
tsconfig: undefined,
|
|
103
109
|
json: false,
|
|
104
|
-
strict: false,
|
|
105
110
|
strictConfig: false,
|
|
106
111
|
requireGates: false,
|
|
112
|
+
requireWriteHook: undefined,
|
|
107
113
|
init: false,
|
|
108
114
|
installAgentGates: false,
|
|
115
|
+
compact: false,
|
|
109
116
|
tools: undefined,
|
|
110
117
|
force: false,
|
|
111
118
|
skillsOnly: false,
|
|
@@ -122,6 +129,8 @@ function parseArgs(argv) {
|
|
|
122
129
|
applyPolicyPack: undefined,
|
|
123
130
|
watch: false,
|
|
124
131
|
beginner: false,
|
|
132
|
+
openReport: false,
|
|
133
|
+
noOpenReport: false,
|
|
125
134
|
version: false,
|
|
126
135
|
help: false,
|
|
127
136
|
};
|
|
@@ -135,16 +144,19 @@ function parseArgs(argv) {
|
|
|
135
144
|
for (let i = 2; i < argv.length; i += 1) {
|
|
136
145
|
const arg = argv[i];
|
|
137
146
|
if (arg === '--json') args.json = true;
|
|
138
|
-
else if (arg === '--strict') {
|
|
139
|
-
args.strict = true;
|
|
147
|
+
else if (arg === '--strict' || arg === '--strict-merge') {
|
|
140
148
|
args.strictConfig = true;
|
|
141
149
|
args.requireGates = true;
|
|
142
150
|
}
|
|
143
151
|
else if (arg === '--strict-config') args.strictConfig = true;
|
|
144
152
|
else if (arg === '--require-gates') args.requireGates = true;
|
|
153
|
+
else if (arg === '--require-write-hook') {
|
|
154
|
+
args.requireWriteHook = requireValue(arg, i++).trim().toLowerCase();
|
|
155
|
+
}
|
|
145
156
|
else if (arg === '--init') args.init = true;
|
|
146
157
|
else if (arg === '--preset') args.preset = requireValue(arg, i++);
|
|
147
158
|
else if (arg === '--install-agent-gates') args.installAgentGates = true;
|
|
159
|
+
else if (arg === '--compact') args.compact = true;
|
|
148
160
|
else if (arg === '--tools') {
|
|
149
161
|
// Consume the next arg only when it isn't another flag (same rule as --baseline),
|
|
150
162
|
// so `--tools --force` can't silently eat --force as a "tool name".
|
|
@@ -183,6 +195,8 @@ function parseArgs(argv) {
|
|
|
183
195
|
}
|
|
184
196
|
else if (arg === '--reset-origin') args.resetOrigin = true;
|
|
185
197
|
else if (arg === '--no-archive') args.noArchive = true;
|
|
198
|
+
else if (arg === '--open') args.openReport = true;
|
|
199
|
+
else if (arg === '--no-open') args.noOpenReport = true;
|
|
186
200
|
else if (arg === '--baseline' || arg === '--update-baseline') {
|
|
187
201
|
if (arg === '--update-baseline') args.updateBaseline = true;
|
|
188
202
|
// optional path value: consume the next arg only when it isn't another flag
|
|
@@ -214,7 +228,7 @@ function usage() {
|
|
|
214
228
|
return [
|
|
215
229
|
'Usage: arkgate-check | ark-check (identical bins; product name ArkGate)',
|
|
216
230
|
' ark-check --version',
|
|
217
|
-
' ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict | --strict-config] [--require-gates] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
|
|
231
|
+
' ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-merge | --strict | --strict-config] [--require-gates] [--require-write-hook <host>] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
|
|
218
232
|
' ark-check --coverage [--json] per-layer file counts + full unclassified list (report only, exit 0)',
|
|
219
233
|
' ark-check --plan [--json] classified remediation plan (mechanical-safe / judgment / deferred) + goal; report only',
|
|
220
234
|
' ark-check --recommend [--json] [--write-plan] application-shape plan; --write-plan emits ark-adoption-plan.json',
|
|
@@ -224,10 +238,11 @@ function usage() {
|
|
|
224
238
|
' ark-check --adopt-contract [--write] expand include + UI patterns from ungoverned dirs (contract adopt)',
|
|
225
239
|
' ark-check --ratchet-cores when raw graph is green (0 violations; baseline ignored), set optional:false on populated cores only (writes ark.config.json)',
|
|
226
240
|
' ark-check --watch re-run the check when governed files change (debounced)',
|
|
227
|
-
' ark-check --report [file.html] [--beginner] [--reset-origin] [--no-archive]',
|
|
241
|
+
' ark-check --report [file.html] [--beginner] [--reset-origin] [--no-archive] [--open|--no-open]',
|
|
228
242
|
' HTML report + snapshots under .ark/reports/ (origin once, latest each run, history JSON)',
|
|
243
|
+
' Best-effort open in browser (local TTY). No-op if open fails. --no-open / ARK_NO_OPEN_REPORT=1 to skip; --open forces open.',
|
|
229
244
|
' ark-check --init [--preset hexagonal|layered|feature-sliced|monorepo|ui-surface|vertical-slice|ddd-bounded-contexts|clean-architecture|onion-architecture] [--force]',
|
|
230
|
-
' ark-check --install-agent-gates [--tools claude,cursor,codex,grok] [--skills-only] [--codex-home] [--force]',
|
|
245
|
+
' ark-check --install-agent-gates [--tools claude,cursor,codex,grok] [--require-write-hook <host>] [--skills-only] [--codex-home] [--force]',
|
|
231
246
|
' ark-check --update-baseline [file] freeze current violations (default .ark-baseline.json)',
|
|
232
247
|
' ark-check --print-config eleven-layer',
|
|
233
248
|
'',
|
|
@@ -267,8 +282,12 @@ function usage() {
|
|
|
267
282
|
'',
|
|
268
283
|
'Config warnings are advisory by default and are included in JSON output.',
|
|
269
284
|
'Use --strict-config to make config warnings fail the check.',
|
|
270
|
-
'Use --strict for the fail-closed CI profile: --strict-config + --require-gates',
|
|
271
|
-
'plus the security diagnostics surfaced by doctor.',
|
|
285
|
+
'Use --strict-merge for the fail-closed CI profile: --strict-config + --require-gates',
|
|
286
|
+
'plus the security diagnostics surfaced by doctor. --strict is a compatibility alias.',
|
|
287
|
+
'This merge profile never depends on an editor/agent hook.',
|
|
288
|
+
'Add --require-write-hook claude|grok to validate a hard local write boundary for that',
|
|
289
|
+
'specific host. Cursor and Codex expose advisory MCP tools plus the shared CI check;',
|
|
290
|
+
'merge blocking requires repository policy to make that status required.',
|
|
272
291
|
'',
|
|
273
292
|
'--require-gates fails the check when AGENTS.md, .mcp.json, or the generated CI',
|
|
274
293
|
'workflow is missing, so "installed but never configured" is a red CI. Combine it',
|
|
@@ -276,7 +295,8 @@ function usage() {
|
|
|
276
295
|
'',
|
|
277
296
|
'--install-agent-gates writes AGENTS.md, .mcp.json, and the CI workflow for every',
|
|
278
297
|
'project, plus tool-specific templates. Known tools: claude, cursor, codex, grok',
|
|
279
|
-
'(
|
|
298
|
+
'(Claude/Grok hard-write hooks; Cursor/Codex advisory MCP; shared CI check for all) and',
|
|
299
|
+
'windsurf, cline, copilot, kiro, roo, continue, gemini',
|
|
280
300
|
'(instruction-tier rule files derived from the same contract).',
|
|
281
301
|
'It also installs the /ark-* skills shipped in templates/skills/ into each',
|
|
282
302
|
'detected tool\'s command location (.claude/skills/, .cursor/commands/,',
|
|
@@ -290,7 +310,7 @@ function usage() {
|
|
|
290
310
|
'Pass --tools to pick which tool configs to write; otherwise they are auto-detected',
|
|
291
311
|
'from their config directories (.claude/, .cursor/, .codex/, .grok/, .windsurf/,',
|
|
292
312
|
'.clinerules/, .kiro/, .roo/, .continue/, .gemini/; copilot is explicit-only).',
|
|
293
|
-
'claude+cursor+codex are written when nothing is detected.',
|
|
313
|
+
'claude+cursor+codex+grok are written when nothing is detected.',
|
|
294
314
|
'',
|
|
295
315
|
'Generate a starter 11-layer config:',
|
|
296
316
|
' ark-check --print-config eleven-layer > ark.config.json',
|
|
@@ -305,25 +325,12 @@ function readConfig(root, configPath) {
|
|
|
305
325
|
? configPath
|
|
306
326
|
: path.join(root, configPath);
|
|
307
327
|
if (!fs.existsSync(fullPath)) {
|
|
308
|
-
return
|
|
309
|
-
include: ['src'],
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
};
|
|
328
|
+
return loadArkConfigContract(
|
|
329
|
+
{ include: ['src'], layers: [], rules: DEFAULT_RULES },
|
|
330
|
+
fullPath
|
|
331
|
+
).config;
|
|
313
332
|
}
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
include: raw.include ?? ['src'],
|
|
317
|
-
layers: raw.layers ?? [],
|
|
318
|
-
rules: raw.rules ?? DEFAULT_RULES,
|
|
319
|
-
...(raw.exclude ? { exclude: raw.exclude } : {}),
|
|
320
|
-
...(raw.excludeGenerated !== undefined ? { excludeGenerated: raw.excludeGenerated } : {}),
|
|
321
|
-
...(raw.cyclePolicy ? { cyclePolicy: raw.cyclePolicy } : {}),
|
|
322
|
-
...(raw.dynamicImportAllowlist
|
|
323
|
-
? { dynamicImportAllowlist: raw.dynamicImportAllowlist }
|
|
324
|
-
: {}),
|
|
325
|
-
...(raw.safety ? { safety: raw.safety } : {}),
|
|
326
|
-
};
|
|
333
|
+
return parseArkConfigJson(fs.readFileSync(fullPath, 'utf8'), fullPath).config;
|
|
327
334
|
}
|
|
328
335
|
|
|
329
336
|
/**
|
|
@@ -976,16 +983,41 @@ async function main() {
|
|
|
976
983
|
return;
|
|
977
984
|
}
|
|
978
985
|
|
|
979
|
-
if (args.requireGates) {
|
|
980
|
-
|
|
981
|
-
if (args.
|
|
982
|
-
|
|
986
|
+
if (args.requireGates || args.requireWriteHook) {
|
|
987
|
+
let writeRequest = null;
|
|
988
|
+
if (args.requireWriteHook) {
|
|
989
|
+
writeRequest = validateHardWriteRequest({
|
|
990
|
+
root: args.root,
|
|
991
|
+
host: args.requireWriteHook,
|
|
992
|
+
tools: [args.requireWriteHook],
|
|
993
|
+
force: true,
|
|
994
|
+
});
|
|
995
|
+
if (!writeRequest.ok) {
|
|
996
|
+
const payload = {
|
|
997
|
+
ok: false,
|
|
998
|
+
error: 'unsupported-enforcement-profile',
|
|
999
|
+
message: writeRequest.error,
|
|
1000
|
+
};
|
|
1001
|
+
if (args.json) console.log(JSON.stringify(payload, null, 2));
|
|
1002
|
+
else console.error(writeRequest.error);
|
|
1003
|
+
process.exitCode = 2;
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const missing = args.requireGates ? missingGates(args.root) : [];
|
|
1009
|
+
if (
|
|
1010
|
+
writeRequest?.host &&
|
|
1011
|
+
!detectWritePathCapabilities(args.root, writeRequest.host).capabilities['hard-write']
|
|
1012
|
+
) {
|
|
1013
|
+
missing.push(`${writeRequest.host} hard-write hook`);
|
|
983
1014
|
}
|
|
984
1015
|
if (missing.length > 0) {
|
|
985
1016
|
const payload = {
|
|
986
1017
|
ok: false,
|
|
987
1018
|
error: 'missing-gates',
|
|
988
1019
|
missing,
|
|
1020
|
+
...(writeRequest?.host ? { writeHost: writeRequest.host } : {}),
|
|
989
1021
|
};
|
|
990
1022
|
if (args.json) {
|
|
991
1023
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -994,7 +1026,12 @@ async function main() {
|
|
|
994
1026
|
for (const relativePath of missing) {
|
|
995
1027
|
console.error(` - ${relativePath}`);
|
|
996
1028
|
}
|
|
997
|
-
|
|
1029
|
+
const installArgs = writeRequest?.host
|
|
1030
|
+
? `--install-agent-gates --tools ${writeRequest.host} --require-write-hook ${writeRequest.host}`
|
|
1031
|
+
: '--install-agent-gates';
|
|
1032
|
+
console.error(
|
|
1033
|
+
`\nRun \`${arkCommand(args.root, 'ark', 'init')}\` (or \`${arkCommand(args.root, 'ark-check', installArgs)}\`) to configure enforcement.`
|
|
1034
|
+
);
|
|
998
1035
|
}
|
|
999
1036
|
process.exitCode = 1;
|
|
1000
1037
|
return;
|
|
@@ -1004,7 +1041,18 @@ async function main() {
|
|
|
1004
1041
|
// When --require-gates is the only intent (no config/architecture run needed),
|
|
1005
1042
|
// callers still get a clear signal from the exit code and the human-mode line.
|
|
1006
1043
|
if (!args.json) {
|
|
1007
|
-
|
|
1044
|
+
if (args.requireGates) {
|
|
1045
|
+
const compactHost = compactRouterHost(args.root);
|
|
1046
|
+
console.log(
|
|
1047
|
+
'Ark gates present (merge profile): ' +
|
|
1048
|
+
(compactHost
|
|
1049
|
+
? `AGENTS.md, compact host registration (${compactHost})`
|
|
1050
|
+
: REQUIRED_GATE_FILES.join(', '))
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
if (writeRequest?.host) {
|
|
1054
|
+
console.log(`Ark hard-write hook present for ${writeRequest.host}.`);
|
|
1055
|
+
}
|
|
1008
1056
|
}
|
|
1009
1057
|
}
|
|
1010
1058
|
|
|
@@ -1277,10 +1325,28 @@ async function main() {
|
|
|
1277
1325
|
}
|
|
1278
1326
|
}
|
|
1279
1327
|
}
|
|
1328
|
+
|
|
1329
|
+
// Best-effort: open the report in the default browser. If it opens, fine;
|
|
1330
|
+
// if not (headless, no GUI, spawn error), do nothing — never fail the check.
|
|
1331
|
+
// Skipped in CI / Vitest / ARK_NO_OPEN_REPORT; --open / --no-open override.
|
|
1332
|
+
if (
|
|
1333
|
+
shouldOpenHtmlReport({
|
|
1334
|
+
force: Boolean(args.openReport),
|
|
1335
|
+
noOpen: Boolean(args.noOpenReport) || Boolean(args.json),
|
|
1336
|
+
})
|
|
1337
|
+
) {
|
|
1338
|
+
openHtmlInBrowser(reportPath);
|
|
1339
|
+
}
|
|
1280
1340
|
}
|
|
1281
1341
|
|
|
1282
1342
|
if (args.json) {
|
|
1343
|
+
const adapterResult = createAdapterResult({
|
|
1344
|
+
valid: ok,
|
|
1345
|
+
violations: activeViolations.map(enrichViolationWithFixClass),
|
|
1346
|
+
warnings,
|
|
1347
|
+
});
|
|
1283
1348
|
console.log(JSON.stringify({
|
|
1349
|
+
...adapterResult,
|
|
1284
1350
|
ok,
|
|
1285
1351
|
violations: activeViolations.map(enrichViolationWithFixClass),
|
|
1286
1352
|
suppressedViolations: suppressed.length,
|
|
@@ -1385,10 +1451,19 @@ async function main() {
|
|
|
1385
1451
|
const parts = [];
|
|
1386
1452
|
if (codexHomeGap.missing > 0) parts.push(`${codexHomeGap.missing} missing`);
|
|
1387
1453
|
if (codexHomeGap.stale > 0) parts.push(`${codexHomeGap.stale} outdated`);
|
|
1454
|
+
// Advisory always; when session host is known and not Codex, say so so
|
|
1455
|
+
// /ark-upgrade does not chase home prompts as Incomplete.
|
|
1456
|
+
const activeHost = detectActiveAgentHost();
|
|
1457
|
+
const deferredNote =
|
|
1458
|
+
activeHost != null && activeHost !== 'codex'
|
|
1459
|
+
? ' Deferred unless you use Codex — not a blocker for Grok/Claude/Cursor. '
|
|
1460
|
+
: ' ';
|
|
1388
1461
|
console.log(
|
|
1389
1462
|
color.dim(
|
|
1390
|
-
`/ark-* skills in ${codexPromptsDir()} are behind this Ark (${parts.join(', ')})
|
|
1391
|
-
|
|
1463
|
+
`/ark-* skills in ${codexPromptsDir()} are behind this Ark (${parts.join(', ')}).` +
|
|
1464
|
+
deferredNote +
|
|
1465
|
+
`Codex loads them from $CODEX_HOME/prompts, not the repo. ` +
|
|
1466
|
+
`When using Codex: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --codex-home --force')}`
|
|
1392
1467
|
)
|
|
1393
1468
|
);
|
|
1394
1469
|
}
|
|
@@ -1430,9 +1505,13 @@ async function runWatchMode(args) {
|
|
|
1430
1505
|
const target = path.join(args.root, entry);
|
|
1431
1506
|
if (!fs.existsSync(target)) continue;
|
|
1432
1507
|
try {
|
|
1433
|
-
fs.watch(target, { recursive: true }, rerun);
|
|
1508
|
+
const watcher = fs.watch(target, { recursive: true }, rerun);
|
|
1509
|
+
watcher.on('error', () => {
|
|
1510
|
+
watcher.close();
|
|
1511
|
+
watchByPolling(target, rerun);
|
|
1512
|
+
});
|
|
1434
1513
|
} catch {
|
|
1435
|
-
|
|
1514
|
+
watchByPolling(target, rerun);
|
|
1436
1515
|
}
|
|
1437
1516
|
}
|
|
1438
1517
|
|
|
@@ -1440,7 +1519,45 @@ async function runWatchMode(args) {
|
|
|
1440
1519
|
await new Promise(() => {});
|
|
1441
1520
|
}
|
|
1442
1521
|
|
|
1522
|
+
function watchByPolling(target, onChange) {
|
|
1523
|
+
let previous = watchFingerprint(target);
|
|
1524
|
+
setInterval(() => {
|
|
1525
|
+
const current = watchFingerprint(target);
|
|
1526
|
+
if (current === previous) return;
|
|
1527
|
+
previous = current;
|
|
1528
|
+
onChange();
|
|
1529
|
+
}, 250);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function watchFingerprint(target) {
|
|
1533
|
+
const pending = [target];
|
|
1534
|
+
const entries = [];
|
|
1535
|
+
while (pending.length > 0) {
|
|
1536
|
+
const current = pending.pop();
|
|
1537
|
+
let stat;
|
|
1538
|
+
try {
|
|
1539
|
+
stat = fs.statSync(current);
|
|
1540
|
+
} catch {
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
entries.push(`${current}:${stat.mtimeMs}:${stat.size}`);
|
|
1544
|
+
if (!stat.isDirectory()) continue;
|
|
1545
|
+
try {
|
|
1546
|
+
for (const name of fs.readdirSync(current)) pending.push(path.join(current, name));
|
|
1547
|
+
} catch {
|
|
1548
|
+
// A concurrent delete is represented by the next fingerprint.
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return entries.sort().join('|');
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1443
1554
|
main().catch((error) => {
|
|
1444
|
-
console.error(
|
|
1555
|
+
console.error(
|
|
1556
|
+
process.env.ARK_DEBUG_STACK === '1' && error instanceof Error
|
|
1557
|
+
? error.stack
|
|
1558
|
+
: error instanceof Error
|
|
1559
|
+
? error.message
|
|
1560
|
+
: String(error)
|
|
1561
|
+
);
|
|
1445
1562
|
process.exitCode = 2;
|
|
1446
1563
|
});
|
package/bin/ark-mcp.mjs
CHANGED
|
@@ -58,6 +58,8 @@ import {
|
|
|
58
58
|
import { createImportTargetResolver } from './lib/import-resolve.mjs';
|
|
59
59
|
import { validateWithAutoPatch, resolveImportFileAbs } from './lib/auto-patch.mjs';
|
|
60
60
|
import { composePrepareWrite } from './lib/prepare-write.mjs';
|
|
61
|
+
import { loadArkConfigContract } from './lib/config-contract.mjs';
|
|
62
|
+
import { ARK_ANALYSIS_RESULT_SCHEMA, createAdapterResult } from './lib/adapter-contract.mjs';
|
|
61
63
|
|
|
62
64
|
const arkCheckBin = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
|
|
63
65
|
|
|
@@ -120,6 +122,11 @@ function readJson(file, { required } = {}) {
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
function readArkConfig(file, { required } = {}) {
|
|
126
|
+
const raw = readJson(file, { required });
|
|
127
|
+
return raw === undefined ? undefined : loadArkConfigContract(raw, file).config;
|
|
128
|
+
}
|
|
129
|
+
|
|
123
130
|
function resolveInRoot(root, maybePath) {
|
|
124
131
|
if (!maybePath) return undefined;
|
|
125
132
|
return path.isAbsolute(maybePath) ? maybePath : path.join(root, maybePath);
|
|
@@ -173,6 +180,8 @@ function normalizeHookPayload(payload) {
|
|
|
173
180
|
Edit: 'Edit',
|
|
174
181
|
search_replace: 'Edit',
|
|
175
182
|
MultiEdit: 'MultiEdit',
|
|
183
|
+
ApplyPatch: 'ApplyPatch',
|
|
184
|
+
apply_patch: 'ApplyPatch',
|
|
176
185
|
};
|
|
177
186
|
const toolName = nameMap[rawName] ?? rawName;
|
|
178
187
|
const filePath =
|
|
@@ -187,6 +196,70 @@ function normalizeHookPayload(payload) {
|
|
|
187
196
|
};
|
|
188
197
|
}
|
|
189
198
|
|
|
199
|
+
function applyCodexUpdatePatch(current, lines) {
|
|
200
|
+
let source = current.split('\n');
|
|
201
|
+
let cursor = 0;
|
|
202
|
+
const hunks = [];
|
|
203
|
+
let hunk = [];
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
if (line.startsWith('@@')) {
|
|
206
|
+
if (hunk.length > 0) hunks.push(hunk);
|
|
207
|
+
hunk = [];
|
|
208
|
+
} else if (/^[ +\-]/.test(line)) {
|
|
209
|
+
hunk.push(line);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (hunk.length > 0) hunks.push(hunk);
|
|
213
|
+
for (const entries of hunks) {
|
|
214
|
+
const oldLines = entries.filter((line) => !line.startsWith('+')).map((line) => line.slice(1));
|
|
215
|
+
const newLines = entries.filter((line) => !line.startsWith('-')).map((line) => line.slice(1));
|
|
216
|
+
let found = -1;
|
|
217
|
+
for (let at = cursor; at <= source.length - oldLines.length; at += 1) {
|
|
218
|
+
if (oldLines.every((line, index) => source[at + index] === line)) {
|
|
219
|
+
found = at;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (found < 0) return null;
|
|
224
|
+
source.splice(found, oldLines.length, ...newLines);
|
|
225
|
+
cursor = found + newLines.length;
|
|
226
|
+
}
|
|
227
|
+
return source.join('\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function codexPatchWrites(patch, root) {
|
|
231
|
+
if (typeof patch !== 'string' || !patch.includes('*** Begin Patch')) return [];
|
|
232
|
+
const lines = patch.split('\n');
|
|
233
|
+
const writes = [];
|
|
234
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
235
|
+
const match = lines[index].match(/^\*\*\* (Add|Update|Delete) File: (.+)$/);
|
|
236
|
+
if (!match) continue;
|
|
237
|
+
const [, action, relativePath] = match;
|
|
238
|
+
const body = [];
|
|
239
|
+
for (index += 1; index < lines.length && !lines[index].startsWith('*** '); index += 1) {
|
|
240
|
+
body.push(lines[index]);
|
|
241
|
+
}
|
|
242
|
+
index -= 1;
|
|
243
|
+
if (action === 'Delete') continue;
|
|
244
|
+
const filePath = path.resolve(root, relativePath);
|
|
245
|
+
let content;
|
|
246
|
+
if (action === 'Add') {
|
|
247
|
+
content = body.filter((line) => line.startsWith('+')).map((line) => line.slice(1)).join('\n');
|
|
248
|
+
if (body.some((line) => line.startsWith('+'))) content += '\n';
|
|
249
|
+
} else {
|
|
250
|
+
let current;
|
|
251
|
+
try {
|
|
252
|
+
current = fs.readFileSync(filePath, 'utf8');
|
|
253
|
+
} catch {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
content = applyCodexUpdatePatch(current, body);
|
|
257
|
+
}
|
|
258
|
+
if (typeof content === 'string') writes.push({ filePath, content });
|
|
259
|
+
}
|
|
260
|
+
return writes;
|
|
261
|
+
}
|
|
262
|
+
|
|
190
263
|
/**
|
|
191
264
|
* Compute the file content a Write/Edit/MultiEdit is about to produce. Edits are applied
|
|
192
265
|
* to the CURRENT on-disk file so the gate judges the real post-edit state, not the edit
|
|
@@ -231,7 +304,27 @@ function runHook(gate, config, args, ts) {
|
|
|
231
304
|
return;
|
|
232
305
|
}
|
|
233
306
|
|
|
307
|
+
runHookPayload(payload, gate, config, args, ts);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function runHookPayload(payload, gate, config, args, ts) {
|
|
234
311
|
const { toolName, toolInput, grokStyle } = normalizeHookPayload(payload);
|
|
312
|
+
if (toolName === 'ApplyPatch') {
|
|
313
|
+
const patch = toolInput.patch ?? toolInput.input ?? toolInput.content;
|
|
314
|
+
for (const write of codexPatchWrites(patch, args.root)) {
|
|
315
|
+
runHookPayload(
|
|
316
|
+
{
|
|
317
|
+
tool_name: 'Write',
|
|
318
|
+
tool_input: { file_path: write.filePath, content: write.content },
|
|
319
|
+
},
|
|
320
|
+
gate,
|
|
321
|
+
config,
|
|
322
|
+
args,
|
|
323
|
+
ts
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
235
328
|
const filePath = toolInput.file_path;
|
|
236
329
|
if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) return;
|
|
237
330
|
if (typeof filePath !== 'string' || !SOURCE_FILE.test(filePath) || filePath.endsWith('.d.ts')) {
|
|
@@ -292,6 +385,11 @@ function runHook(gate, config, args, ts) {
|
|
|
292
385
|
return false;
|
|
293
386
|
});
|
|
294
387
|
if (newViolations.length === 0) return;
|
|
388
|
+
const normalizedRel = rel.split(path.sep).join('/');
|
|
389
|
+
const adapterResult = createAdapterResult({
|
|
390
|
+
valid: false,
|
|
391
|
+
violations: newViolations.map((violation) => ({ ...violation, file: normalizedRel })),
|
|
392
|
+
});
|
|
295
393
|
|
|
296
394
|
const lines = newViolations.map(
|
|
297
395
|
(violation) =>
|
|
@@ -333,9 +431,10 @@ function runHook(gate, config, args, ts) {
|
|
|
333
431
|
if (repair) {
|
|
334
432
|
// Structured envelope for any host that can re-inject. Never writes the file.
|
|
335
433
|
const repairPayload = {
|
|
434
|
+
...adapterResult,
|
|
336
435
|
mode: 'repair',
|
|
337
436
|
decision: 'deny',
|
|
338
|
-
filePath:
|
|
437
|
+
filePath: normalizedRel,
|
|
339
438
|
...(layer ? { layer } : {}),
|
|
340
439
|
...(autoPatch
|
|
341
440
|
? {
|
|
@@ -361,6 +460,7 @@ function runHook(gate, config, args, ts) {
|
|
|
361
460
|
JSON.stringify({
|
|
362
461
|
decision: 'deny',
|
|
363
462
|
reason: message,
|
|
463
|
+
analysis: adapterResult,
|
|
364
464
|
...(repair && autoPatch ? { autoPatch } : {}),
|
|
365
465
|
...(repair ? { repair: true } : {}),
|
|
366
466
|
}) + '\n'
|
|
@@ -460,11 +560,11 @@ async function main() {
|
|
|
460
560
|
const ts = await loadOptionalTypeScript();
|
|
461
561
|
|
|
462
562
|
const config =
|
|
463
|
-
(configPath ?
|
|
464
|
-
|
|
465
|
-
layers: [],
|
|
466
|
-
|
|
467
|
-
|
|
563
|
+
(configPath ? readArkConfig(configPath, { required: args.configExplicit }) : undefined) ??
|
|
564
|
+
loadArkConfigContract(
|
|
565
|
+
{ include: ['src'], layers: [], rules: DEFAULT_RULES },
|
|
566
|
+
configPath ?? 'ark.config.json'
|
|
567
|
+
).config;
|
|
468
568
|
if (!config.layers || config.layers.length === 0) {
|
|
469
569
|
process.stderr.write(
|
|
470
570
|
'[ark-mcp] warning: no layers configured — file→layer inference from config patterns ' +
|
|
@@ -610,6 +710,7 @@ async function main() {
|
|
|
610
710
|
},
|
|
611
711
|
required: ['source'],
|
|
612
712
|
},
|
|
713
|
+
outputSchema: ARK_ANALYSIS_RESULT_SCHEMA,
|
|
613
714
|
},
|
|
614
715
|
{
|
|
615
716
|
name: 'ark_check',
|
|
@@ -632,6 +733,7 @@ async function main() {
|
|
|
632
733
|
},
|
|
633
734
|
},
|
|
634
735
|
},
|
|
736
|
+
outputSchema: ARK_ANALYSIS_RESULT_SCHEMA,
|
|
635
737
|
},
|
|
636
738
|
{
|
|
637
739
|
name: 'ark_coverage',
|
|
@@ -820,12 +922,17 @@ async function main() {
|
|
|
820
922
|
validate: validateOnce,
|
|
821
923
|
resolveTargetAbs: resolveImportFileAbs,
|
|
822
924
|
});
|
|
925
|
+
const adapterResult = createAdapterResult({
|
|
926
|
+
valid: result.valid,
|
|
927
|
+
violations: result.violations,
|
|
928
|
+
});
|
|
823
929
|
return {
|
|
824
930
|
content: [
|
|
825
931
|
{
|
|
826
932
|
type: 'text',
|
|
827
933
|
text: JSON.stringify(
|
|
828
934
|
{
|
|
935
|
+
...adapterResult,
|
|
829
936
|
valid: result.valid,
|
|
830
937
|
violations: result.violations,
|
|
831
938
|
...(result.autoPatch ? { autoPatch: result.autoPatch } : {}),
|
|
@@ -836,6 +943,7 @@ async function main() {
|
|
|
836
943
|
),
|
|
837
944
|
},
|
|
838
945
|
],
|
|
946
|
+
structuredContent: adapterResult,
|
|
839
947
|
isError: !result.valid,
|
|
840
948
|
};
|
|
841
949
|
}
|
|
@@ -862,6 +970,11 @@ async function main() {
|
|
|
862
970
|
}
|
|
863
971
|
return {
|
|
864
972
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
973
|
+
structuredContent: {
|
|
974
|
+
schemaVersion: data.schemaVersion,
|
|
975
|
+
valid: data.valid,
|
|
976
|
+
diagnostics: data.diagnostics,
|
|
977
|
+
},
|
|
865
978
|
isError: data.ok === false,
|
|
866
979
|
};
|
|
867
980
|
}
|