arkgate 4.1.1 → 4.2.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 +81 -3
- package/README.md +15 -4
- package/bin/ark-check-runtime.mjs +16 -5
- package/bin/ark-mcp-runtime.mjs +766 -64
- package/bin/lib/agent-gates.mjs +1 -0
- package/bin/lib/ci-and-commands.mjs +16 -7
- package/bin/lib/codex-home.mjs +90 -8
- package/bin/lib/design-smells.mjs +71 -9
- package/bin/lib/doctor-plan.mjs +36 -36
- package/bin/lib/effective-contract-load.mjs +73 -9
- package/bin/lib/enforcement-state.mjs +1 -1
- package/bin/lib/gate-files.mjs +441 -9
- package/bin/lib/github-enforcement.mjs +16 -3
- package/bin/lib/hook-templates.mjs +12 -11
- package/bin/lib/html-report-evolution.mjs +114 -0
- package/bin/lib/html-report.mjs +11 -89
- package/bin/lib/import-resolve.mjs +33 -11
- package/bin/lib/install-activation.mjs +87 -0
- package/bin/lib/install-migrate.mjs +66 -50
- package/bin/lib/managed-upgrade.mjs +10 -41
- package/bin/lib/mcp-adoption.mjs +15 -5
- package/bin/lib/pilot-loop.mjs +25 -8
- package/bin/lib/project-identity.mjs +103 -0
- package/bin/lib/report-snapshot-context.mjs +28 -0
- package/bin/lib/resident-hook.mjs +33 -9
- package/bin/lib/rules-inventory.mjs +100 -8
- package/bin/lib/skill-install.mjs +272 -22
- package/bin/lib/skill-write.mjs +899 -0
- package/bin/lib/start-preview.mjs +84 -1
- package/bin/lib/upgrade-command.mjs +2 -5
- package/dist/index.cjs +13 -13
- package/dist/index.d.ts +194 -2
- package/dist/index.js +13 -13
- package/docs/README.md +5 -3
- package/docs/agent-guide.md +110 -14
- package/docs/ai-gates.md +103 -18
- package/docs/assets/ark-write-gate.svg +2 -2
- package/docs/enthusiast/how-to-agent-gates.md +6 -0
- package/docs/package-surface.md +14 -9
- package/docs/product-voice.md +13 -1
- package/package.json +3 -1
- package/schemas/ark.project-identity.schema.json +116 -0
- package/server.json +2 -2
- package/templates/skills/ark-adopt.md +9 -0
- package/templates/skills/ark-architect.md +12 -2
- package/templates/skills/ark-autopilot.md +9 -0
- package/templates/skills/ark-contract.md +11 -1
- package/templates/skills/ark-coverage.md +9 -0
- package/templates/skills/ark-explain.md +13 -1
- package/templates/skills/ark-explore.md +9 -0
- package/templates/skills/ark-fix.md +10 -1
- package/templates/skills/ark-loop.md +11 -2
- package/templates/skills/ark-place.md +17 -6
- package/templates/skills/ark-runtime.md +8 -0
- package/templates/skills/ark-think.md +14 -2
- package/templates/skills/ark-upgrade.md +9 -0
package/bin/ark-mcp-runtime.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import readline from 'node:readline';
|
|
6
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
7
|
import { spawnSync } from 'node:child_process';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import {
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
detectTsPackageRoots,
|
|
18
19
|
resolveIncludeRoots,
|
|
19
20
|
} from './ark-shared.mjs';
|
|
20
|
-
import { effectiveCapabilityDeny } from './lib/analysis-engine.mjs';
|
|
21
|
+
import { effectiveCapabilityDeny, stableSerialize } from './lib/analysis-engine.mjs';
|
|
21
22
|
import { createImportTargetResolver } from './lib/import-resolve.mjs';
|
|
22
23
|
import { validateWithAutoPatch, resolveImportFileAbs } from './lib/auto-patch.mjs';
|
|
23
24
|
import { composePrepareWrite } from './lib/prepare-write.mjs';
|
|
@@ -28,33 +29,34 @@ import {
|
|
|
28
29
|
inventoryToExtractionCard,
|
|
29
30
|
} from './lib/rules-inventory.mjs';
|
|
30
31
|
import { ARK_ANALYSIS_RESULT_SCHEMA, createAdapterResult } from './lib/adapter-contract.mjs';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
32
|
+
import {
|
|
33
|
+
ARK_PROJECT_IDENTITY_SCHEMA,
|
|
34
|
+
PROJECT_BINDING_SCHEMA,
|
|
35
|
+
PROJECT_EXPECTATION_SCHEMA,
|
|
36
|
+
createProjectId,
|
|
37
|
+
createProjectIdentity,
|
|
38
|
+
} from './lib/project-identity.mjs';
|
|
39
|
+
|
|
40
|
+
function arkRulesCatalogForManifest(snapshot) {
|
|
41
|
+
if (snapshot?.errors?.length || !snapshot?.arkRules) return {};
|
|
42
|
+
const structure = (snapshot.arkRules.structure ?? []).map((r) => ({
|
|
43
|
+
id: r.id,
|
|
44
|
+
sensor: r.sensor,
|
|
45
|
+
mode: r.mode,
|
|
46
|
+
layer: r.provenance?.layer,
|
|
47
|
+
sourceFile: r.provenance?.sourceFile,
|
|
48
|
+
}));
|
|
49
|
+
const invariants = (snapshot.arkRules.invariants ?? []).map((r) => ({
|
|
50
|
+
id: r.id,
|
|
51
|
+
description: r.description,
|
|
52
|
+
aggregate: r.aggregate,
|
|
53
|
+
mode: r.mode,
|
|
54
|
+
layer: r.provenance?.layer,
|
|
55
|
+
sourceFile: r.provenance?.sourceFile,
|
|
56
|
+
coverage: r.coverage,
|
|
57
|
+
}));
|
|
58
|
+
if (structure.length === 0 && invariants.length === 0) return {};
|
|
59
|
+
return { arkRulesCatalog: { structure, invariants } };
|
|
58
60
|
}
|
|
59
61
|
import { loadTypeScript } from './lib/typescript-host.mjs';
|
|
60
62
|
import { validateSnippetAnalysis } from './lib/snippet-analysis.mjs';
|
|
@@ -76,6 +78,7 @@ import {
|
|
|
76
78
|
residentDoctorEnvironment,
|
|
77
79
|
residentEnvironmentIdentity,
|
|
78
80
|
residentHookEndpoint,
|
|
81
|
+
residentInvocationIdentity,
|
|
79
82
|
startResidentHookServer,
|
|
80
83
|
} from './lib/resident-hook.mjs';
|
|
81
84
|
import { resolveArchitectureSnapshot } from './lib/architecture-scan.mjs';
|
|
@@ -110,6 +113,7 @@ function parseArgs(argv) {
|
|
|
110
113
|
hookRepair: false,
|
|
111
114
|
failOnNewSmells: false,
|
|
112
115
|
sessionContext: false,
|
|
116
|
+
rootEnv: [],
|
|
113
117
|
};
|
|
114
118
|
for (let i = 2; i < argv.length; i += 1) {
|
|
115
119
|
const a = argv[i];
|
|
@@ -120,7 +124,13 @@ function parseArgs(argv) {
|
|
|
120
124
|
} else if (a === '--session-context') args.sessionContext = true;
|
|
121
125
|
else if (a === '--fail-on-new-smells') args.failOnNewSmells = true;
|
|
122
126
|
else if (a === '--root') args.root = path.resolve(argv[++i]);
|
|
123
|
-
else if (a === '--
|
|
127
|
+
else if (a === '--root-env') {
|
|
128
|
+
const names = String(argv[++i] ?? '')
|
|
129
|
+
.split(',')
|
|
130
|
+
.map((name) => name.trim())
|
|
131
|
+
.filter((name) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(name));
|
|
132
|
+
args.rootEnv.push(...names);
|
|
133
|
+
} else if (a === '--config') {
|
|
124
134
|
args.config = argv[++i];
|
|
125
135
|
args.configExplicit = true;
|
|
126
136
|
} else if (a === '--manifest') args.manifest = argv[++i];
|
|
@@ -131,6 +141,13 @@ function parseArgs(argv) {
|
|
|
131
141
|
args.hookRepair = true;
|
|
132
142
|
}
|
|
133
143
|
if (envTruthy('ARK_FAIL_ON_NEW_SMELLS')) args.failOnNewSmells = true;
|
|
144
|
+
for (const name of args.rootEnv) {
|
|
145
|
+
const value = process.env[name];
|
|
146
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
147
|
+
args.root = path.resolve(value.trim());
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
134
151
|
return args;
|
|
135
152
|
}
|
|
136
153
|
|
|
@@ -156,9 +173,66 @@ function readArkConfig(file, { required } = {}) {
|
|
|
156
173
|
return raw === undefined ? undefined : loadArkConfigContract(raw, file).config;
|
|
157
174
|
}
|
|
158
175
|
|
|
159
|
-
function
|
|
176
|
+
function sha256Hex(value) {
|
|
177
|
+
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizeWindowsDriveLetter(candidate) {
|
|
181
|
+
if (process.platform !== 'win32' || !/^[A-Za-z]:[\\/]/.test(candidate)) {
|
|
182
|
+
return candidate;
|
|
183
|
+
}
|
|
184
|
+
return `${candidate[0].toUpperCase()}${candidate.slice(1)}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function canonicalPathIncludingMissing(candidate) {
|
|
188
|
+
const absolute = normalizeWindowsDriveLetter(path.resolve(candidate));
|
|
189
|
+
let existing = absolute;
|
|
190
|
+
const missing = [];
|
|
191
|
+
while (!fs.existsSync(existing)) {
|
|
192
|
+
try {
|
|
193
|
+
if (fs.lstatSync(existing).isSymbolicLink()) {
|
|
194
|
+
const error = new Error(
|
|
195
|
+
`PROJECT_ROOT_MISMATCH: cannot canonicalize dangling symlink ${existing}.`
|
|
196
|
+
);
|
|
197
|
+
error.code = 'PROJECT_ROOT_MISMATCH';
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (error?.code === 'PROJECT_ROOT_MISMATCH') throw error;
|
|
202
|
+
}
|
|
203
|
+
const parent = path.dirname(existing);
|
|
204
|
+
if (parent === existing) return absolute;
|
|
205
|
+
missing.unshift(path.basename(existing));
|
|
206
|
+
existing = parent;
|
|
207
|
+
}
|
|
208
|
+
return normalizeWindowsDriveLetter(path.join(fs.realpathSync(existing), ...missing));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function pathIsWithin(root, candidate) {
|
|
212
|
+
const relative = path.relative(root, candidate);
|
|
213
|
+
return (
|
|
214
|
+
relative === '' ||
|
|
215
|
+
(relative !== '..' &&
|
|
216
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
217
|
+
!path.isAbsolute(relative))
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resolveContainedProjectPath(root, maybePath, label) {
|
|
160
222
|
if (!maybePath) return undefined;
|
|
161
|
-
|
|
223
|
+
const requested = path.isAbsolute(maybePath)
|
|
224
|
+
? maybePath
|
|
225
|
+
: path.resolve(root, maybePath);
|
|
226
|
+
const canonical = canonicalPathIncludingMissing(requested);
|
|
227
|
+
if (!pathIsWithin(root, canonical)) {
|
|
228
|
+
const error = new Error(
|
|
229
|
+
`PROJECT_ROOT_MISMATCH: ${label} resolves outside the configured ArkGate root ` +
|
|
230
|
+
`(${canonical} is not inside ${root}).`
|
|
231
|
+
);
|
|
232
|
+
error.code = 'PROJECT_ROOT_MISMATCH';
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
return canonical;
|
|
162
236
|
}
|
|
163
237
|
|
|
164
238
|
function inferLayer(filePath, config, root) {
|
|
@@ -831,7 +905,7 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
831
905
|
'machine-readable source (still hard-blocks; host re-injects).',
|
|
832
906
|
]
|
|
833
907
|
: []),
|
|
834
|
-
'Fix the violations and retry.
|
|
908
|
+
'Fix the violations and retry. Call the project-bound ark_manifest MCP tool for the architecture contract.',
|
|
835
909
|
].join('\n');
|
|
836
910
|
output.stderr(message + '\n');
|
|
837
911
|
|
|
@@ -963,12 +1037,19 @@ function captureResidentHook(payload, gate, config, args, ts, request) {
|
|
|
963
1037
|
let stdout = '';
|
|
964
1038
|
let stderr = '';
|
|
965
1039
|
let status = 0;
|
|
1040
|
+
const requestArgs = {
|
|
1041
|
+
...args,
|
|
1042
|
+
root: request.root,
|
|
1043
|
+
config: request.config,
|
|
1044
|
+
manifest: request.manifest ?? undefined,
|
|
1045
|
+
tsconfig: request.tsconfig ?? undefined,
|
|
1046
|
+
};
|
|
966
1047
|
runHookPayload(
|
|
967
1048
|
payload,
|
|
968
1049
|
gate,
|
|
969
1050
|
config,
|
|
970
1051
|
{
|
|
971
|
-
...
|
|
1052
|
+
...requestArgs,
|
|
972
1053
|
hookRepair: request.hookRepair === true,
|
|
973
1054
|
failOnNewSmells: request.failOnNewSmells === true,
|
|
974
1055
|
},
|
|
@@ -994,10 +1075,8 @@ function sameResidentInvocation(request, args, kind) {
|
|
|
994
1075
|
request?.protocolVersion === RESIDENT_HOOK_PROTOCOL_VERSION &&
|
|
995
1076
|
request?.kind === kind &&
|
|
996
1077
|
typeof request.root === 'string' &&
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
(request.manifest ?? null) === (args.manifest ?? null) &&
|
|
1000
|
-
(request.tsconfig ?? null) === (args.tsconfig ?? null)
|
|
1078
|
+
JSON.stringify(residentInvocationIdentity(request)) ===
|
|
1079
|
+
JSON.stringify(residentInvocationIdentity(args))
|
|
1001
1080
|
);
|
|
1002
1081
|
}
|
|
1003
1082
|
|
|
@@ -1034,7 +1113,7 @@ function createResidentDoctorSession(args, config, ts) {
|
|
|
1034
1113
|
const resolutionInputs = snapshotInputPaths(snapshot.inputs);
|
|
1035
1114
|
const ledger = createResidentInputLedger([...after.paths, ...resolutionInputs]);
|
|
1036
1115
|
if (!ledger.matches([...after.paths, ...resolutionInputs])) return null;
|
|
1037
|
-
return { files: after.files, ledger, resolutionInputs, rules, snapshot };
|
|
1116
|
+
return { root: args.root, files: after.files, ledger, resolutionInputs, rules, snapshot };
|
|
1038
1117
|
}
|
|
1039
1118
|
|
|
1040
1119
|
function verifyResidentDoctorSession(session) {
|
|
@@ -1043,7 +1122,10 @@ function verifyResidentDoctorSession(session) {
|
|
|
1043
1122
|
|
|
1044
1123
|
function renderResidentDoctor(session, args, config, ts) {
|
|
1045
1124
|
let stdout = '';
|
|
1046
|
-
|
|
1125
|
+
const files = session.files.map((file) =>
|
|
1126
|
+
path.resolve(args.root, path.relative(session.root, file))
|
|
1127
|
+
);
|
|
1128
|
+
runDoctor(args.root, config, files, session.rules, session.snapshot.result.violations, true, {
|
|
1047
1129
|
configPath: path.isAbsolute(args.config) ? args.config : path.join(args.root, args.config),
|
|
1048
1130
|
configMissing: !fs.existsSync(
|
|
1049
1131
|
path.isAbsolute(args.config) ? args.config : path.join(args.root, args.config)
|
|
@@ -1145,7 +1227,18 @@ async function startResidentHookControl({ args, gate, config, ts, loadedTypeScri
|
|
|
1145
1227
|
if (!doctorSession || !verified) {
|
|
1146
1228
|
return { protocolVersion: RESIDENT_HOOK_PROTOCOL_VERSION, fallback: true };
|
|
1147
1229
|
}
|
|
1148
|
-
const response = renderResidentDoctor(
|
|
1230
|
+
const response = renderResidentDoctor(
|
|
1231
|
+
doctorSession,
|
|
1232
|
+
{
|
|
1233
|
+
...args,
|
|
1234
|
+
root: request.root,
|
|
1235
|
+
config: request.config,
|
|
1236
|
+
manifest: request.manifest ?? undefined,
|
|
1237
|
+
tsconfig: request.tsconfig ?? undefined,
|
|
1238
|
+
},
|
|
1239
|
+
config,
|
|
1240
|
+
ts
|
|
1241
|
+
);
|
|
1149
1242
|
if (!verifyResidentDoctorSession(doctorSession)) {
|
|
1150
1243
|
doctorSession = undefined;
|
|
1151
1244
|
return { protocolVersion: RESIDENT_HOOK_PROTOCOL_VERSION, fallback: true };
|
|
@@ -1211,7 +1304,7 @@ function printSessionContext(config, profile, forbiddenGlobals, args, configPath
|
|
|
1211
1304
|
|
|
1212
1305
|
const denied = (profile.rules ?? []).filter((rule) => !rule.allowed).length;
|
|
1213
1306
|
lines.push(
|
|
1214
|
-
`Rules: ${denied} denied layer edge(s). Full contract:
|
|
1307
|
+
`Rules: ${denied} denied layer edge(s). Full contract: project-bound ark_manifest MCP tool.`
|
|
1215
1308
|
);
|
|
1216
1309
|
|
|
1217
1310
|
// Advisory output: a malformed baseline must not abort the summary.
|
|
@@ -1243,8 +1336,17 @@ function printSessionContext(config, profile, forbiddenGlobals, args, configPath
|
|
|
1243
1336
|
}
|
|
1244
1337
|
|
|
1245
1338
|
export async function runArkMcp({ hookInput } = {}) {
|
|
1339
|
+
const processStartedAt = new Date().toISOString();
|
|
1340
|
+
const runtimeId = randomUUID();
|
|
1246
1341
|
const args = parseArgs(process.argv);
|
|
1247
|
-
const
|
|
1342
|
+
const requestedRoot = args.root;
|
|
1343
|
+
const resolvedRoot = canonicalPathIncludingMissing(requestedRoot);
|
|
1344
|
+
// MCP identity and file containment use the canonical workspace. Hook payloads,
|
|
1345
|
+
// however, carry paths in the caller's spelling (macOS commonly aliases /var to
|
|
1346
|
+
// /private/var); keep that spelling so same-workspace writes are not mistaken for
|
|
1347
|
+
// out-of-root paths.
|
|
1348
|
+
if (!args.hook && !args.sessionContext) args.root = resolvedRoot;
|
|
1349
|
+
const configPath = resolveContainedProjectPath(resolvedRoot, args.config, 'ark.config.json');
|
|
1248
1350
|
|
|
1249
1351
|
// SessionStart contract injection is only meaningful in Ark-governed projects. Bail
|
|
1250
1352
|
// out silently (before loading dist) when there is no config, so the hook is safe
|
|
@@ -1271,8 +1373,11 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1271
1373
|
);
|
|
1272
1374
|
}
|
|
1273
1375
|
|
|
1274
|
-
const manifestPath =
|
|
1376
|
+
const manifestPath = resolveContainedProjectPath(resolvedRoot, args.manifest, 'project manifest');
|
|
1275
1377
|
const projectManifest = manifestPath ? readJson(manifestPath, { required: true }) : undefined;
|
|
1378
|
+
if (args.tsconfig) {
|
|
1379
|
+
args.tsconfig = resolveContainedProjectPath(resolvedRoot, args.tsconfig, 'TypeScript config');
|
|
1380
|
+
}
|
|
1276
1381
|
args.projectManifest = projectManifest;
|
|
1277
1382
|
|
|
1278
1383
|
const intents = Array.isArray(projectManifest?.intents)
|
|
@@ -1388,6 +1493,23 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1388
1493
|
return;
|
|
1389
1494
|
}
|
|
1390
1495
|
|
|
1496
|
+
const effectiveArkRulesSnapshot = (() => {
|
|
1497
|
+
try {
|
|
1498
|
+
const loaded = loadEffectiveArkRulesFromDisk(args.root, config);
|
|
1499
|
+
return {
|
|
1500
|
+
arkRules: loaded.arkRules ?? null,
|
|
1501
|
+
warnings: loaded.warnings ?? [],
|
|
1502
|
+
errors: loaded.errors ?? [],
|
|
1503
|
+
};
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
return {
|
|
1506
|
+
arkRules: null,
|
|
1507
|
+
warnings: [],
|
|
1508
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
})();
|
|
1512
|
+
|
|
1391
1513
|
const residentHookControl = await startResidentHookControl({
|
|
1392
1514
|
args,
|
|
1393
1515
|
gate,
|
|
@@ -1400,8 +1522,428 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1400
1522
|
|
|
1401
1523
|
const SERVER_INFO = { name: 'arkgate', version: ark.version };
|
|
1402
1524
|
const DEFAULT_PROTOCOL = '2024-11-05';
|
|
1525
|
+
const projectIdentity = createProjectIdentity({
|
|
1526
|
+
projectId: createProjectId(resolvedRoot, configPath, sha256Hex),
|
|
1527
|
+
resolvedRoot,
|
|
1528
|
+
resolvedConfigPath: configPath,
|
|
1529
|
+
arkgateVersion: ark.version,
|
|
1530
|
+
contractHash: `sha256:${sha256Hex(
|
|
1531
|
+
stableSerialize({
|
|
1532
|
+
config,
|
|
1533
|
+
projectManifest: projectManifest ?? null,
|
|
1534
|
+
arkRules: effectiveArkRulesSnapshot,
|
|
1535
|
+
})
|
|
1536
|
+
)}`,
|
|
1537
|
+
contractSource: projectManifest
|
|
1538
|
+
? 'manifest'
|
|
1539
|
+
: fs.existsSync(configPath)
|
|
1540
|
+
? 'project'
|
|
1541
|
+
: 'default-profile',
|
|
1542
|
+
runtimeId,
|
|
1543
|
+
processStartedAt,
|
|
1544
|
+
});
|
|
1545
|
+
const projectIdentityOutputSchema = {
|
|
1546
|
+
type: ARK_PROJECT_IDENTITY_SCHEMA.type,
|
|
1547
|
+
additionalProperties: ARK_PROJECT_IDENTITY_SCHEMA.additionalProperties,
|
|
1548
|
+
required: ARK_PROJECT_IDENTITY_SCHEMA.required,
|
|
1549
|
+
properties: ARK_PROJECT_IDENTITY_SCHEMA.properties,
|
|
1550
|
+
};
|
|
1551
|
+
const verificationOutputSchema = {
|
|
1552
|
+
anyOf: [{ type: 'boolean' }, { const: 'unverified' }],
|
|
1553
|
+
};
|
|
1554
|
+
const checkVerdictOutputSchema = {
|
|
1555
|
+
type: 'object',
|
|
1556
|
+
additionalProperties: false,
|
|
1557
|
+
required: ['identity', 'completeness', 'graph', 'coverage', 'gates', 'overallOk'],
|
|
1558
|
+
properties: {
|
|
1559
|
+
identity: {
|
|
1560
|
+
type: 'object',
|
|
1561
|
+
additionalProperties: false,
|
|
1562
|
+
required: ['status', 'ok'],
|
|
1563
|
+
properties: {
|
|
1564
|
+
status: { enum: ['matched', 'unverified', 'mismatch'] },
|
|
1565
|
+
ok: { type: 'boolean' },
|
|
1566
|
+
},
|
|
1567
|
+
},
|
|
1568
|
+
completeness: {
|
|
1569
|
+
type: 'object',
|
|
1570
|
+
additionalProperties: false,
|
|
1571
|
+
required: ['status', 'ok'],
|
|
1572
|
+
properties: {
|
|
1573
|
+
status: { type: 'string', minLength: 1 },
|
|
1574
|
+
ok: { type: 'boolean' },
|
|
1575
|
+
},
|
|
1576
|
+
},
|
|
1577
|
+
graph: {
|
|
1578
|
+
type: 'object',
|
|
1579
|
+
additionalProperties: false,
|
|
1580
|
+
required: ['ok', 'violations'],
|
|
1581
|
+
properties: {
|
|
1582
|
+
ok: { type: 'boolean' },
|
|
1583
|
+
violations: { type: ['integer', 'null'], minimum: 0 },
|
|
1584
|
+
},
|
|
1585
|
+
},
|
|
1586
|
+
coverage: {
|
|
1587
|
+
type: 'object',
|
|
1588
|
+
additionalProperties: false,
|
|
1589
|
+
required: ['ok', 'governedPercent', 'unclassified', 'emptyScope'],
|
|
1590
|
+
properties: {
|
|
1591
|
+
ok: { type: 'boolean' },
|
|
1592
|
+
governedPercent: { type: ['number', 'null'], minimum: 0, maximum: 100 },
|
|
1593
|
+
unclassified: { type: ['integer', 'null'], minimum: 0 },
|
|
1594
|
+
emptyScope: { type: ['boolean', 'null'] },
|
|
1595
|
+
},
|
|
1596
|
+
},
|
|
1597
|
+
gates: {
|
|
1598
|
+
type: 'object',
|
|
1599
|
+
additionalProperties: false,
|
|
1600
|
+
required: [
|
|
1601
|
+
'ok',
|
|
1602
|
+
'localWriteActive',
|
|
1603
|
+
'advisoryMcpActive',
|
|
1604
|
+
'advisoryMcpRuntimeObserved',
|
|
1605
|
+
'ciMergeActive',
|
|
1606
|
+
],
|
|
1607
|
+
properties: {
|
|
1608
|
+
ok: { type: 'boolean' },
|
|
1609
|
+
localWriteActive: verificationOutputSchema,
|
|
1610
|
+
advisoryMcpActive: { const: true },
|
|
1611
|
+
advisoryMcpRuntimeObserved: { const: true },
|
|
1612
|
+
ciMergeActive: verificationOutputSchema,
|
|
1613
|
+
},
|
|
1614
|
+
},
|
|
1615
|
+
overallOk: { type: 'boolean' },
|
|
1616
|
+
},
|
|
1617
|
+
};
|
|
1618
|
+
const analysisResultWithProjectSchema = {
|
|
1619
|
+
type: ARK_ANALYSIS_RESULT_SCHEMA.type,
|
|
1620
|
+
additionalProperties: ARK_ANALYSIS_RESULT_SCHEMA.additionalProperties,
|
|
1621
|
+
allOf: ARK_ANALYSIS_RESULT_SCHEMA.allOf,
|
|
1622
|
+
required: [
|
|
1623
|
+
...(ARK_ANALYSIS_RESULT_SCHEMA.required ?? []),
|
|
1624
|
+
'projectIdentity',
|
|
1625
|
+
'binding',
|
|
1626
|
+
'authoritative',
|
|
1627
|
+
],
|
|
1628
|
+
properties: {
|
|
1629
|
+
...ARK_ANALYSIS_RESULT_SCHEMA.properties,
|
|
1630
|
+
projectIdentity: projectIdentityOutputSchema,
|
|
1631
|
+
binding: PROJECT_BINDING_SCHEMA,
|
|
1632
|
+
authoritative: { type: 'boolean' },
|
|
1633
|
+
verdict: checkVerdictOutputSchema,
|
|
1634
|
+
},
|
|
1635
|
+
};
|
|
1636
|
+
const projectBindingErrorSchema = {
|
|
1637
|
+
type: 'object',
|
|
1638
|
+
additionalProperties: false,
|
|
1639
|
+
required: ['ok', 'error', 'projectIdentity', 'binding', 'authoritative'],
|
|
1640
|
+
properties: {
|
|
1641
|
+
ok: { const: false },
|
|
1642
|
+
error: {
|
|
1643
|
+
type: 'object',
|
|
1644
|
+
additionalProperties: false,
|
|
1645
|
+
required: ['code', 'message'],
|
|
1646
|
+
properties: {
|
|
1647
|
+
code: { type: 'string', minLength: 1 },
|
|
1648
|
+
message: { type: 'string', minLength: 1 },
|
|
1649
|
+
},
|
|
1650
|
+
},
|
|
1651
|
+
projectIdentity: projectIdentityOutputSchema,
|
|
1652
|
+
binding: PROJECT_BINDING_SCHEMA,
|
|
1653
|
+
authoritative: { type: 'boolean' },
|
|
1654
|
+
},
|
|
1655
|
+
};
|
|
1656
|
+
const projectAwareAnalysisResultSchema = {
|
|
1657
|
+
oneOf: [analysisResultWithProjectSchema, projectBindingErrorSchema],
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
function unverifiedBinding() {
|
|
1661
|
+
return {
|
|
1662
|
+
status: 'unverified',
|
|
1663
|
+
authoritative: false,
|
|
1664
|
+
message:
|
|
1665
|
+
'No project expectation was supplied. Call ark_identity with project.expectedRoot ' +
|
|
1666
|
+
'before treating MCP evidence as authoritative.',
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function mismatchBinding(code, message, expectation = {}) {
|
|
1671
|
+
return {
|
|
1672
|
+
status: 'mismatch',
|
|
1673
|
+
authoritative: false,
|
|
1674
|
+
...(expectation.expectedRoot ? { expectedRoot: expectation.expectedRoot } : {}),
|
|
1675
|
+
...(expectation.expectedProjectId
|
|
1676
|
+
? { expectedProjectId: expectation.expectedProjectId }
|
|
1677
|
+
: {}),
|
|
1678
|
+
code,
|
|
1679
|
+
message,
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
function nestedProjectConfig(expectedRoot) {
|
|
1684
|
+
let current = expectedRoot;
|
|
1685
|
+
while (pathIsWithin(resolvedRoot, current) && current !== resolvedRoot) {
|
|
1686
|
+
const candidate = path.join(current, 'ark.config.json');
|
|
1687
|
+
if (fs.existsSync(candidate)) return fs.realpathSync(candidate);
|
|
1688
|
+
const parent = path.dirname(current);
|
|
1689
|
+
if (parent === current) break;
|
|
1690
|
+
current = parent;
|
|
1691
|
+
}
|
|
1692
|
+
return undefined;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function bindingForExpectation(expectation) {
|
|
1696
|
+
if (expectation === undefined) return unverifiedBinding();
|
|
1697
|
+
if (!expectation || typeof expectation !== 'object' || Array.isArray(expectation)) {
|
|
1698
|
+
return mismatchBinding(
|
|
1699
|
+
'INVALID_PROJECT_EXPECTATION',
|
|
1700
|
+
'project must be an object containing expectedRoot and/or expectedProjectId.'
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
const rawExpectedRoot = expectation.expectedRoot;
|
|
1704
|
+
const rawExpectedProjectId = expectation.expectedProjectId;
|
|
1705
|
+
if (
|
|
1706
|
+
rawExpectedRoot !== undefined &&
|
|
1707
|
+
(typeof rawExpectedRoot !== 'string' ||
|
|
1708
|
+
rawExpectedRoot.trim() === '' ||
|
|
1709
|
+
!path.isAbsolute(rawExpectedRoot))
|
|
1710
|
+
) {
|
|
1711
|
+
return mismatchBinding(
|
|
1712
|
+
'INVALID_PROJECT_EXPECTATION',
|
|
1713
|
+
'project.expectedRoot must be a non-empty absolute path.'
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
if (
|
|
1717
|
+
rawExpectedProjectId !== undefined &&
|
|
1718
|
+
(typeof rawExpectedProjectId !== 'string' ||
|
|
1719
|
+
!/^sha256:[a-f0-9]{64}$/.test(rawExpectedProjectId))
|
|
1720
|
+
) {
|
|
1721
|
+
return mismatchBinding(
|
|
1722
|
+
'INVALID_PROJECT_EXPECTATION',
|
|
1723
|
+
'project.expectedProjectId must be a sha256:<64 lowercase hex> identity.'
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
if (rawExpectedRoot === undefined && rawExpectedProjectId === undefined) {
|
|
1727
|
+
return unverifiedBinding();
|
|
1728
|
+
}
|
|
1729
|
+
if (rawExpectedRoot === undefined) {
|
|
1730
|
+
if (rawExpectedProjectId !== projectIdentity.projectId) {
|
|
1731
|
+
return mismatchBinding(
|
|
1732
|
+
'PROJECT_ID_MISMATCH',
|
|
1733
|
+
`Expected project id ${rawExpectedProjectId}, but this MCP is bound to ` +
|
|
1734
|
+
`${projectIdentity.projectId}.`,
|
|
1735
|
+
{ expectedProjectId: rawExpectedProjectId }
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
return {
|
|
1739
|
+
status: 'unverified',
|
|
1740
|
+
authoritative: false,
|
|
1741
|
+
expectedProjectId: rawExpectedProjectId,
|
|
1742
|
+
message:
|
|
1743
|
+
'project.expectedProjectId matched, but expectedRoot is required for an ' +
|
|
1744
|
+
'authoritative workspace binding.',
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
let expectedRoot;
|
|
1749
|
+
try {
|
|
1750
|
+
expectedRoot = canonicalPathIncludingMissing(rawExpectedRoot);
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
return mismatchBinding(
|
|
1753
|
+
'PROJECT_ROOT_MISMATCH',
|
|
1754
|
+
error instanceof Error ? error.message : String(error),
|
|
1755
|
+
{ expectedRoot: rawExpectedRoot, expectedProjectId: rawExpectedProjectId }
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
if (!pathIsWithin(resolvedRoot, expectedRoot)) {
|
|
1759
|
+
return mismatchBinding(
|
|
1760
|
+
'PROJECT_ROOT_MISMATCH',
|
|
1761
|
+
`Expected workspace ${expectedRoot}, but this ArkGate MCP is bound to ${resolvedRoot}.`,
|
|
1762
|
+
{ expectedRoot, expectedProjectId: rawExpectedProjectId }
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
const nestedConfig = nestedProjectConfig(expectedRoot);
|
|
1766
|
+
if (nestedConfig && nestedConfig !== configPath) {
|
|
1767
|
+
return mismatchBinding(
|
|
1768
|
+
'PROJECT_ROOT_MISMATCH',
|
|
1769
|
+
`Expected workspace ${expectedRoot} belongs to a nested ArkGate project at ` +
|
|
1770
|
+
`${nestedConfig}, but this MCP is bound to ${configPath}.`,
|
|
1771
|
+
{ expectedRoot, expectedProjectId: rawExpectedProjectId }
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
if (
|
|
1776
|
+
rawExpectedProjectId !== undefined &&
|
|
1777
|
+
rawExpectedProjectId !== projectIdentity.projectId
|
|
1778
|
+
) {
|
|
1779
|
+
return mismatchBinding(
|
|
1780
|
+
'PROJECT_ID_MISMATCH',
|
|
1781
|
+
`Expected project id ${rawExpectedProjectId}, but this MCP is bound to ` +
|
|
1782
|
+
`${projectIdentity.projectId}.`,
|
|
1783
|
+
{ expectedRoot, expectedProjectId: rawExpectedProjectId }
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
if (expectedRoot !== resolvedRoot && rawExpectedProjectId === undefined) {
|
|
1787
|
+
return {
|
|
1788
|
+
status: 'unverified',
|
|
1789
|
+
authoritative: false,
|
|
1790
|
+
expectedRoot,
|
|
1791
|
+
message:
|
|
1792
|
+
`Expected workspace ${expectedRoot} is inside this MCP project, but an exact project ` +
|
|
1793
|
+
'root is required for the initial authoritative handshake. Call ark_identity at the ' +
|
|
1794
|
+
`project root ${resolvedRoot}, then reuse its projectIdentity.projectId for descendant calls.`,
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
return {
|
|
1798
|
+
status: 'matched',
|
|
1799
|
+
authoritative: true,
|
|
1800
|
+
...(expectedRoot ? { expectedRoot } : {}),
|
|
1801
|
+
...(rawExpectedProjectId ? { expectedProjectId: rawExpectedProjectId } : {}),
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function bindingForToolPaths(toolName, toolArguments, currentBinding) {
|
|
1806
|
+
if (currentBinding.status === 'mismatch') return currentBinding;
|
|
1807
|
+
const candidates = [];
|
|
1808
|
+
if (['validate_code', 'ark_place', 'ark_prepare_write'].includes(toolName)) {
|
|
1809
|
+
candidates.push(toolArguments?.filePath);
|
|
1810
|
+
}
|
|
1811
|
+
if (toolName === 'ark_prepare_change') {
|
|
1812
|
+
for (const change of toolArguments?.changes ?? []) candidates.push(change?.path);
|
|
1813
|
+
for (const file of toolArguments?.changeMap?.files ?? []) candidates.push(file?.path);
|
|
1814
|
+
}
|
|
1815
|
+
for (const candidate of candidates) {
|
|
1816
|
+
if (typeof candidate !== 'string' || candidate === '') continue;
|
|
1817
|
+
const absolute = path.isAbsolute(candidate)
|
|
1818
|
+
? candidate
|
|
1819
|
+
: path.resolve(resolvedRoot, candidate);
|
|
1820
|
+
let canonical;
|
|
1821
|
+
try {
|
|
1822
|
+
canonical = canonicalPathIncludingMissing(absolute);
|
|
1823
|
+
} catch (error) {
|
|
1824
|
+
return mismatchBinding(
|
|
1825
|
+
'PROJECT_ROOT_MISMATCH',
|
|
1826
|
+
error instanceof Error ? error.message : String(error),
|
|
1827
|
+
{
|
|
1828
|
+
expectedRoot: currentBinding.expectedRoot,
|
|
1829
|
+
expectedProjectId: currentBinding.expectedProjectId,
|
|
1830
|
+
}
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
if (!pathIsWithin(resolvedRoot, canonical)) {
|
|
1834
|
+
return mismatchBinding(
|
|
1835
|
+
'PROJECT_ROOT_MISMATCH',
|
|
1836
|
+
`Tool ${toolName} received ${canonical}, which is outside the MCP project root ` +
|
|
1837
|
+
`${resolvedRoot}.`,
|
|
1838
|
+
{
|
|
1839
|
+
expectedRoot: currentBinding.expectedRoot,
|
|
1840
|
+
expectedProjectId: currentBinding.expectedProjectId,
|
|
1841
|
+
}
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return currentBinding;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function contextFor(binding) {
|
|
1849
|
+
return {
|
|
1850
|
+
projectIdentity,
|
|
1851
|
+
binding,
|
|
1852
|
+
authoritative: binding.authoritative,
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
function withProjectContext(result, binding) {
|
|
1857
|
+
const context = contextFor(binding);
|
|
1858
|
+
const content = Array.isArray(result?.content)
|
|
1859
|
+
? result.content.map((block) => {
|
|
1860
|
+
if (block?.type !== 'text' || typeof block.text !== 'string') return block;
|
|
1861
|
+
let body;
|
|
1862
|
+
try {
|
|
1863
|
+
body = JSON.parse(block.text);
|
|
1864
|
+
} catch {
|
|
1865
|
+
body = result?.isError
|
|
1866
|
+
? {
|
|
1867
|
+
ok: false,
|
|
1868
|
+
error: {
|
|
1869
|
+
code: 'ARK_TOOL_ERROR',
|
|
1870
|
+
message: block.text,
|
|
1871
|
+
},
|
|
1872
|
+
}
|
|
1873
|
+
: { ok: true, result: block.text };
|
|
1874
|
+
}
|
|
1875
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
1876
|
+
body = { ok: !result?.isError, result: body };
|
|
1877
|
+
}
|
|
1878
|
+
return {
|
|
1879
|
+
...block,
|
|
1880
|
+
text: JSON.stringify({ ...body, ...context }, null, 2),
|
|
1881
|
+
};
|
|
1882
|
+
})
|
|
1883
|
+
: [{ type: 'text', text: JSON.stringify(context, null, 2) }];
|
|
1884
|
+
let structuredContent =
|
|
1885
|
+
result?.structuredContent &&
|
|
1886
|
+
typeof result.structuredContent === 'object' &&
|
|
1887
|
+
!Array.isArray(result.structuredContent)
|
|
1888
|
+
? { ...result.structuredContent, ...context }
|
|
1889
|
+
: undefined;
|
|
1890
|
+
if (!structuredContent && content[0]?.type === 'text') {
|
|
1891
|
+
try {
|
|
1892
|
+
const parsed = JSON.parse(content[0].text);
|
|
1893
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1894
|
+
structuredContent = parsed;
|
|
1895
|
+
}
|
|
1896
|
+
} catch {
|
|
1897
|
+
/* withProjectContext always renders text blocks as JSON above */
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return {
|
|
1901
|
+
...result,
|
|
1902
|
+
...context,
|
|
1903
|
+
content,
|
|
1904
|
+
...(structuredContent ? { structuredContent } : {}),
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function bindingFailureResult(binding) {
|
|
1909
|
+
return withProjectContext(
|
|
1910
|
+
{
|
|
1911
|
+
content: [
|
|
1912
|
+
{
|
|
1913
|
+
type: 'text',
|
|
1914
|
+
text: JSON.stringify({
|
|
1915
|
+
ok: false,
|
|
1916
|
+
error: {
|
|
1917
|
+
code: binding.code,
|
|
1918
|
+
message: binding.message,
|
|
1919
|
+
},
|
|
1920
|
+
}),
|
|
1921
|
+
},
|
|
1922
|
+
],
|
|
1923
|
+
isError: true,
|
|
1924
|
+
},
|
|
1925
|
+
binding
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1403
1928
|
|
|
1404
1929
|
const TOOLS = [
|
|
1930
|
+
{
|
|
1931
|
+
name: 'ark_identity',
|
|
1932
|
+
description:
|
|
1933
|
+
'Return the canonical ArkGate project, config, contract, and live MCP runtime identity. ' +
|
|
1934
|
+
'Pass project.expectedRoot and/or expectedProjectId to verify this process before ' +
|
|
1935
|
+
'trusting any architecture evidence.',
|
|
1936
|
+
inputSchema: { type: 'object', properties: {} },
|
|
1937
|
+
},
|
|
1938
|
+
{
|
|
1939
|
+
name: 'ark_manifest',
|
|
1940
|
+
description:
|
|
1941
|
+
'Return the machine-readable architecture contract with an authoritative project ' +
|
|
1942
|
+
'binding when project.expectedRoot matches this MCP project. Prefer this tool over ' +
|
|
1943
|
+
'the ark://manifest compatibility resource, whose standard MCP read shape cannot ' +
|
|
1944
|
+
'carry a portable project expectation.',
|
|
1945
|
+
inputSchema: { type: 'object', properties: {} },
|
|
1946
|
+
},
|
|
1405
1947
|
{
|
|
1406
1948
|
name: 'validate_code',
|
|
1407
1949
|
description:
|
|
@@ -1428,7 +1970,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1428
1970
|
},
|
|
1429
1971
|
required: ['source'],
|
|
1430
1972
|
},
|
|
1431
|
-
outputSchema:
|
|
1973
|
+
outputSchema: projectAwareAnalysisResultSchema,
|
|
1432
1974
|
},
|
|
1433
1975
|
{
|
|
1434
1976
|
name: 'ark_check',
|
|
@@ -1436,7 +1978,8 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1436
1978
|
'Run the full Ark architecture check on the project and return structured results ' +
|
|
1437
1979
|
'(layer-import violations, forbidden globals, circular deps, config warnings). Use ' +
|
|
1438
1980
|
'this to answer "is the architecture currently valid?" instead of shelling out to ' +
|
|
1439
|
-
'ark-check.
|
|
1981
|
+
'ark-check. Preserves legacy ok and adds identity/completeness/graph/coverage/gates/' +
|
|
1982
|
+
'overall verdicts. Applies the baseline automatically when one exists. isError when not ok.',
|
|
1440
1983
|
inputSchema: {
|
|
1441
1984
|
type: 'object',
|
|
1442
1985
|
properties: {
|
|
@@ -1451,7 +1994,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1451
1994
|
},
|
|
1452
1995
|
},
|
|
1453
1996
|
},
|
|
1454
|
-
outputSchema:
|
|
1997
|
+
outputSchema: projectAwareAnalysisResultSchema,
|
|
1455
1998
|
},
|
|
1456
1999
|
{
|
|
1457
2000
|
name: 'ark_policy_delta',
|
|
@@ -1602,13 +2145,24 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1602
2145
|
},
|
|
1603
2146
|
];
|
|
1604
2147
|
|
|
2148
|
+
for (const tool of TOOLS) {
|
|
2149
|
+
tool.inputSchema = {
|
|
2150
|
+
...tool.inputSchema,
|
|
2151
|
+
properties: {
|
|
2152
|
+
...(tool.inputSchema.properties ?? {}),
|
|
2153
|
+
project: PROJECT_EXPECTATION_SCHEMA,
|
|
2154
|
+
},
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
|
|
1605
2158
|
const RESOURCES = [
|
|
1606
2159
|
{
|
|
1607
2160
|
uri: 'ark://manifest',
|
|
1608
2161
|
name: 'Ark architectural contract',
|
|
1609
2162
|
description:
|
|
1610
|
-
'
|
|
1611
|
-
'
|
|
2163
|
+
'Compatibility-only architecture contract resource. Standard MCP resource reads cannot ' +
|
|
2164
|
+
'portably carry a project expectation, so this surface is always unverified and ' +
|
|
2165
|
+
'non-authoritative. Use the ark_manifest tool for bound contract evidence.',
|
|
1612
2166
|
mimeType: 'application/json',
|
|
1613
2167
|
},
|
|
1614
2168
|
];
|
|
@@ -1641,10 +2195,15 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1641
2195
|
}));
|
|
1642
2196
|
}
|
|
1643
2197
|
|
|
1644
|
-
function manifestText() {
|
|
2198
|
+
function manifestText(binding = unverifiedBinding()) {
|
|
2199
|
+
const context = contextFor(binding);
|
|
1645
2200
|
if (projectManifest) {
|
|
1646
2201
|
return JSON.stringify(
|
|
1647
|
-
{
|
|
2202
|
+
{
|
|
2203
|
+
...projectManifest,
|
|
2204
|
+
source: projectManifest.source ?? 'manifest',
|
|
2205
|
+
...context,
|
|
2206
|
+
},
|
|
1648
2207
|
null,
|
|
1649
2208
|
2
|
|
1650
2209
|
);
|
|
@@ -1678,7 +2237,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1678
2237
|
...(config.arkRules && typeof config.arkRules === 'object'
|
|
1679
2238
|
? { arkRules: config.arkRules }
|
|
1680
2239
|
: {}),
|
|
1681
|
-
...arkRulesCatalogForManifest(
|
|
2240
|
+
...arkRulesCatalogForManifest(effectiveArkRulesSnapshot),
|
|
1682
2241
|
...(suggestions.length > 0
|
|
1683
2242
|
? {
|
|
1684
2243
|
suggestedLayers: suggestions,
|
|
@@ -1689,12 +2248,36 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1689
2248
|
'inventing an ungoverned location.',
|
|
1690
2249
|
}
|
|
1691
2250
|
: {}),
|
|
2251
|
+
...context,
|
|
1692
2252
|
},
|
|
1693
2253
|
null,
|
|
1694
2254
|
2
|
|
1695
2255
|
);
|
|
1696
2256
|
}
|
|
1697
2257
|
|
|
2258
|
+
function runIdentityTool() {
|
|
2259
|
+
return {
|
|
2260
|
+
content: [
|
|
2261
|
+
{
|
|
2262
|
+
type: 'text',
|
|
2263
|
+
text: JSON.stringify({
|
|
2264
|
+
ok: true,
|
|
2265
|
+
instruction:
|
|
2266
|
+
'Reuse projectIdentity.projectId with project.expectedRoot on subsequent calls.',
|
|
2267
|
+
}),
|
|
2268
|
+
},
|
|
2269
|
+
],
|
|
2270
|
+
isError: false,
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
function runManifestTool(_params, binding) {
|
|
2275
|
+
return {
|
|
2276
|
+
content: [{ type: 'text', text: manifestText(binding) }],
|
|
2277
|
+
isError: false,
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
|
|
1698
2281
|
function runValidate(params) {
|
|
1699
2282
|
const source = params?.arguments?.source;
|
|
1700
2283
|
if (typeof source !== 'string') {
|
|
@@ -1755,7 +2338,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1755
2338
|
);
|
|
1756
2339
|
}
|
|
1757
2340
|
|
|
1758
|
-
function runCheckTool(params) {
|
|
2341
|
+
function runCheckTool(params, binding) {
|
|
1759
2342
|
const strict = params?.arguments?.strict !== false; // default true
|
|
1760
2343
|
const baselineArg = params?.arguments?.baseline;
|
|
1761
2344
|
const baselineExists = fs.existsSync(path.join(args.root, '.ark-baseline.json'));
|
|
@@ -1767,8 +2350,61 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1767
2350
|
if (!data) {
|
|
1768
2351
|
return { content: [{ type: 'text', text: `ark-check produced no JSON:\n${raw}` }], isError: true };
|
|
1769
2352
|
}
|
|
2353
|
+
const { data: coverageData } = runArkCheckJson(['--coverage']);
|
|
2354
|
+
const coverage = coverageData?.coverage;
|
|
2355
|
+
const coverageOk = Boolean(
|
|
2356
|
+
coverage &&
|
|
2357
|
+
coverage.emptyScope === false &&
|
|
2358
|
+
coverage.governed?.percent === 100 &&
|
|
2359
|
+
coverage.unclassified?.count === 0
|
|
2360
|
+
);
|
|
2361
|
+
let writePath;
|
|
2362
|
+
try {
|
|
2363
|
+
writePath = detectWritePathCapabilities(args.root, 'unknown');
|
|
2364
|
+
} catch {
|
|
2365
|
+
writePath = undefined;
|
|
2366
|
+
}
|
|
2367
|
+
const localWriteActive = writePath?.enforcementState?.localWrite?.active ?? 'unverified';
|
|
2368
|
+
const ciMergeActive = writePath?.enforcementState?.ciMerge?.active ?? 'unverified';
|
|
2369
|
+
const gatesOk = localWriteActive === true && ciMergeActive === true;
|
|
2370
|
+
const verdict = {
|
|
2371
|
+
identity: {
|
|
2372
|
+
status: binding.status,
|
|
2373
|
+
ok: binding.status === 'matched',
|
|
2374
|
+
},
|
|
2375
|
+
completeness: {
|
|
2376
|
+
status: data.completeness ?? 'unavailable',
|
|
2377
|
+
ok: data.completeness === 'complete',
|
|
2378
|
+
},
|
|
2379
|
+
graph: {
|
|
2380
|
+
ok: data.valid === true,
|
|
2381
|
+
violations: Array.isArray(data.violations) ? data.violations.length : null,
|
|
2382
|
+
},
|
|
2383
|
+
coverage: {
|
|
2384
|
+
ok: coverageOk,
|
|
2385
|
+
governedPercent: coverage?.governed?.percent ?? null,
|
|
2386
|
+
unclassified: coverage?.unclassified?.count ?? null,
|
|
2387
|
+
emptyScope: coverage?.emptyScope ?? null,
|
|
2388
|
+
},
|
|
2389
|
+
gates: {
|
|
2390
|
+
ok: gatesOk,
|
|
2391
|
+
localWriteActive,
|
|
2392
|
+
advisoryMcpActive: true,
|
|
2393
|
+
advisoryMcpRuntimeObserved: true,
|
|
2394
|
+
ciMergeActive,
|
|
2395
|
+
},
|
|
2396
|
+
overallOk: Boolean(
|
|
2397
|
+
binding.status === 'matched' &&
|
|
2398
|
+
data.ok === true &&
|
|
2399
|
+
data.completeness === 'complete' &&
|
|
2400
|
+
data.valid === true &&
|
|
2401
|
+
coverageOk &&
|
|
2402
|
+
gatesOk
|
|
2403
|
+
),
|
|
2404
|
+
};
|
|
2405
|
+
const payload = { ...data, verdict };
|
|
1770
2406
|
return {
|
|
1771
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
2407
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
1772
2408
|
structuredContent: {
|
|
1773
2409
|
schemaVersion: data.schemaVersion,
|
|
1774
2410
|
mode: data.mode,
|
|
@@ -1780,6 +2416,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1780
2416
|
...(data.resolverIdentity ? { resolverIdentity: data.resolverIdentity } : {}),
|
|
1781
2417
|
...(data.factsHash ? { factsHash: data.factsHash } : {}),
|
|
1782
2418
|
...(data.candidateTreeHash ? { candidateTreeHash: data.candidateTreeHash } : {}),
|
|
2419
|
+
verdict,
|
|
1783
2420
|
},
|
|
1784
2421
|
isError: data.ok === false,
|
|
1785
2422
|
};
|
|
@@ -1882,7 +2519,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1882
2519
|
message: noLayers
|
|
1883
2520
|
? 'This project declares no path-based layers in ark.config.json, so a ' +
|
|
1884
2521
|
'layer cannot be inferred from the path. The gate still enforces the ' +
|
|
1885
|
-
'default 11-layer profile by intent-name prefix —
|
|
2522
|
+
'default 11-layer profile by intent-name prefix — call ark_manifest ' +
|
|
1886
2523
|
'for the layers and validate the actual snippet with validate_code.'
|
|
1887
2524
|
: 'No layer pattern matches this path — code here is UNGOVERNED (no import ' +
|
|
1888
2525
|
'rules enforced). Place it under a directory a layer in ark.config.json ' +
|
|
@@ -2031,20 +2668,31 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2031
2668
|
try {
|
|
2032
2669
|
const governed = collectGovernedFiles(args.root, config);
|
|
2033
2670
|
const fileContents = {};
|
|
2671
|
+
const fileLayers = {};
|
|
2034
2672
|
for (const file of governed.slice(0, 400)) {
|
|
2035
2673
|
const rel = path.relative(args.root, file).split(path.sep).join('/');
|
|
2036
2674
|
try {
|
|
2037
2675
|
fileContents[rel] = fs.readFileSync(file, 'utf8');
|
|
2676
|
+
const layer = layerForFile(args.root, file, config.layers);
|
|
2677
|
+
if (layer) fileLayers[rel] = layer;
|
|
2038
2678
|
} catch {
|
|
2039
2679
|
/* skip */
|
|
2040
2680
|
}
|
|
2041
2681
|
}
|
|
2042
2682
|
const contracted = [];
|
|
2043
|
-
const
|
|
2044
|
-
|
|
2045
|
-
|
|
2683
|
+
for (const rule of effectiveArkRulesSnapshot.arkRules?.structure ?? []) {
|
|
2684
|
+
contracted.push(rule.id);
|
|
2685
|
+
}
|
|
2686
|
+
for (const inv of effectiveArkRulesSnapshot.arkRules?.invariants ?? []) {
|
|
2687
|
+
contracted.push(inv.id);
|
|
2688
|
+
}
|
|
2046
2689
|
const inventory = buildRulesInventory({
|
|
2047
2690
|
fileContents,
|
|
2691
|
+
fileLayers,
|
|
2692
|
+
layerContexts: (config.layers ?? []).map((layer) => ({
|
|
2693
|
+
name: layer.name,
|
|
2694
|
+
intentPrefixes: layer.intentPrefixes ?? [],
|
|
2695
|
+
})),
|
|
2048
2696
|
contractedRuleIds: contracted,
|
|
2049
2697
|
});
|
|
2050
2698
|
const nextPilot =
|
|
@@ -2112,6 +2760,8 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2112
2760
|
}
|
|
2113
2761
|
|
|
2114
2762
|
const TOOL_HANDLERS = {
|
|
2763
|
+
ark_identity: runIdentityTool,
|
|
2764
|
+
ark_manifest: runManifestTool,
|
|
2115
2765
|
validate_code: runValidate,
|
|
2116
2766
|
ark_check: runCheckTool,
|
|
2117
2767
|
ark_policy_delta: runPolicyDeltaTool,
|
|
@@ -2126,7 +2776,16 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2126
2776
|
|
|
2127
2777
|
const send = (msg) => process.stdout.write(`${JSON.stringify(msg)}\n`);
|
|
2128
2778
|
const reply = (id, result) => send({ jsonrpc: '2.0', id, result });
|
|
2129
|
-
const fail = (id, code, message
|
|
2779
|
+
const fail = (id, code, message, binding = unverifiedBinding()) =>
|
|
2780
|
+
send({
|
|
2781
|
+
jsonrpc: '2.0',
|
|
2782
|
+
id,
|
|
2783
|
+
error: {
|
|
2784
|
+
code,
|
|
2785
|
+
message,
|
|
2786
|
+
data: contextFor(binding),
|
|
2787
|
+
},
|
|
2788
|
+
});
|
|
2130
2789
|
|
|
2131
2790
|
function handle(msg) {
|
|
2132
2791
|
const { id, method, params } = msg;
|
|
@@ -2137,38 +2796,81 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2137
2796
|
|
|
2138
2797
|
switch (method) {
|
|
2139
2798
|
case 'initialize':
|
|
2799
|
+
{
|
|
2800
|
+
const binding = bindingForExpectation(params?.project);
|
|
2140
2801
|
reply(id, {
|
|
2141
2802
|
protocolVersion: params?.protocolVersion ?? DEFAULT_PROTOCOL,
|
|
2142
2803
|
capabilities: { tools: {}, resources: {} },
|
|
2143
2804
|
serverInfo: SERVER_INFO,
|
|
2805
|
+
...contextFor(binding),
|
|
2144
2806
|
});
|
|
2807
|
+
}
|
|
2145
2808
|
return;
|
|
2146
2809
|
case 'ping':
|
|
2147
2810
|
reply(id, {});
|
|
2148
2811
|
return;
|
|
2149
2812
|
case 'tools/list':
|
|
2150
|
-
reply(id, { tools: TOOLS });
|
|
2813
|
+
reply(id, { tools: TOOLS, ...contextFor(unverifiedBinding()) });
|
|
2151
2814
|
return;
|
|
2152
2815
|
case 'tools/call': {
|
|
2153
2816
|
const handler = TOOL_HANDLERS[params?.name];
|
|
2154
2817
|
if (!handler) {
|
|
2155
|
-
fail(
|
|
2818
|
+
fail(
|
|
2819
|
+
id,
|
|
2820
|
+
-32602,
|
|
2821
|
+
`Unknown tool: ${params?.name}`,
|
|
2822
|
+
bindingForExpectation(params?.arguments?.project)
|
|
2823
|
+
);
|
|
2156
2824
|
return;
|
|
2157
2825
|
}
|
|
2158
|
-
|
|
2826
|
+
let binding = bindingForExpectation(params?.arguments?.project);
|
|
2827
|
+
binding = bindingForToolPaths(params?.name, params?.arguments, binding);
|
|
2828
|
+
if (binding.status === 'mismatch') {
|
|
2829
|
+
reply(id, bindingFailureResult(binding));
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
try {
|
|
2833
|
+
reply(id, withProjectContext(handler(params, binding), binding));
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2836
|
+
reply(
|
|
2837
|
+
id,
|
|
2838
|
+
withProjectContext(
|
|
2839
|
+
{
|
|
2840
|
+
content: [{ type: 'text', text: message }],
|
|
2841
|
+
isError: true,
|
|
2842
|
+
},
|
|
2843
|
+
binding
|
|
2844
|
+
)
|
|
2845
|
+
);
|
|
2846
|
+
}
|
|
2159
2847
|
return;
|
|
2160
2848
|
}
|
|
2161
2849
|
case 'resources/list':
|
|
2162
|
-
reply(id, { resources: RESOURCES });
|
|
2850
|
+
reply(id, { resources: RESOURCES, ...contextFor(unverifiedBinding()) });
|
|
2163
2851
|
return;
|
|
2164
2852
|
case 'resources/read':
|
|
2853
|
+
{
|
|
2854
|
+
// MCP resources/read has a standard `{ uri }` request shape. Some clients may
|
|
2855
|
+
// forward extension fields, but treating those as a binding would make project
|
|
2856
|
+
// safety host-dependent. Keep this compatibility resource non-authoritative and
|
|
2857
|
+
// require the project-aware ark_manifest tool for trusted contract evidence.
|
|
2858
|
+
const binding = unverifiedBinding();
|
|
2165
2859
|
if (params?.uri !== 'ark://manifest') {
|
|
2166
|
-
fail(id, -32602, `Unknown resource: ${params?.uri}
|
|
2860
|
+
fail(id, -32602, `Unknown resource: ${params?.uri}`, binding);
|
|
2167
2861
|
return;
|
|
2168
2862
|
}
|
|
2169
2863
|
reply(id, {
|
|
2170
|
-
contents: [
|
|
2864
|
+
contents: [
|
|
2865
|
+
{
|
|
2866
|
+
uri: 'ark://manifest',
|
|
2867
|
+
mimeType: 'application/json',
|
|
2868
|
+
text: manifestText(binding),
|
|
2869
|
+
},
|
|
2870
|
+
],
|
|
2871
|
+
...contextFor(binding),
|
|
2171
2872
|
});
|
|
2873
|
+
}
|
|
2172
2874
|
return;
|
|
2173
2875
|
default:
|
|
2174
2876
|
fail(id, -32601, `Method not found: ${method}`);
|