arkgate 4.8.23 → 4.8.25
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 +153 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +41 -91
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +707 -121
- package/bin/ark-mcp-runtime.mjs +448 -169
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +42 -22
- package/bin/ark.mjs +65 -97
- package/bin/lib/adapter-contract-types.mjs +5 -0
- package/bin/lib/adapter-contract.mjs +15 -0
- package/bin/lib/adoption-stance.mjs +2 -2
- package/bin/lib/adr-path.mjs +3 -3
- package/bin/lib/agent-gates.mjs +0 -61
- package/bin/lib/agent-homes.mjs +12 -21
- package/bin/lib/agent-projection-command.mjs +9 -5
- package/bin/lib/ambient-state.mjs +9 -30
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +61 -15
- package/bin/lib/ark-order-doctor.mjs +4 -29
- package/bin/lib/ark-order-facts.mjs +319 -17
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +3 -2
- package/bin/lib/ark-run-doctor.mjs +17 -28
- package/bin/lib/ark-run-facts.mjs +461 -48
- package/bin/lib/ark-run-sensors.mjs +6 -4
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrule-file-hints.mjs +2 -47
- package/bin/lib/arkrules-contract.mjs +124 -133
- package/bin/lib/arkrules-sensors.mjs +55 -252
- package/bin/lib/check-args.mjs +85 -1
- package/bin/lib/ci-and-commands.mjs +24 -78
- package/bin/lib/ci-merge-boundary.mjs +1 -1
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/clone-detection.mjs +474 -0
- package/bin/lib/codex-home.mjs +5 -3
- package/bin/lib/config-contract-slices.mjs +739 -0
- package/bin/lib/config-contract.mjs +42 -607
- package/bin/lib/config-extras.mjs +16 -22
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/contract-smells.mjs +33 -15
- package/bin/lib/core-ratchet.mjs +0 -2
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/deep-module-coach.mjs +2 -4
- package/bin/lib/deploy-path.mjs +13 -38
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/design-smells.mjs +2 -2
- package/bin/lib/diagnostic-catalog.mjs +23 -9
- package/bin/lib/doctor-advisories.mjs +78 -8
- package/bin/lib/doctor-green-cite.mjs +1 -1
- package/bin/lib/doctor-human.mjs +1 -1
- package/bin/lib/doctor-next-actions.mjs +19 -4
- package/bin/lib/doctor-plan.mjs +9 -13
- package/bin/lib/duplication-io.mjs +572 -0
- package/bin/lib/effective-contract-load.mjs +282 -83
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/entry-points-io.mjs +424 -0
- package/bin/lib/extra-merge-teeth.mjs +36 -0
- package/bin/lib/field-install.mjs +38 -14
- package/bin/lib/first-run-help.mjs +10 -5
- package/bin/lib/flat-parent-importers.mjs +139 -0
- package/bin/lib/flat-parent-pilot.mjs +40 -100
- package/bin/lib/gate-files.mjs +58 -90
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +166 -29
- package/bin/lib/golden-pattern.mjs +1 -1
- package/bin/lib/hook-templates.mjs +17 -15
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/html-report-advisories.mjs +4 -0
- package/bin/lib/html-report-depth.mjs +2 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/import-graph-projection.mjs +112 -0
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/improvement-compass-doctor.mjs +1 -24
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage-io.mjs +4 -3
- package/bin/lib/invariant-coverage.mjs +85 -11
- package/bin/lib/invariant-probe-cli.mjs +347 -0
- package/bin/lib/invariant-probe-io.mjs +165 -0
- package/bin/lib/invariant-probe-runner.mjs +268 -0
- package/bin/lib/invariant-probe-sites.mjs +174 -0
- package/bin/lib/invariant-probe-workspace.mjs +183 -0
- package/bin/lib/invariant-probe.mjs +461 -0
- package/bin/lib/invariant-tests-path.mjs +16 -22
- package/bin/lib/literal-path-drift.mjs +4 -2
- package/bin/lib/managed-upgrade.mjs +2 -8
- package/bin/lib/mcp-adoption.mjs +13 -6
- package/bin/lib/mcp-hook-payload.mjs +403 -63
- package/bin/lib/orphan-modules-io.mjs +458 -0
- package/bin/lib/orphan-modules.mjs +251 -0
- package/bin/lib/outside-importers.mjs +197 -0
- package/bin/lib/package-manager.mjs +60 -2
- package/bin/lib/parse-health.mjs +1 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +106 -0
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +219 -14
- package/bin/lib/post-green-path.mjs +1 -1
- package/bin/lib/prepare-change.mjs +115 -7
- package/bin/lib/prepare-write.mjs +2 -2
- package/bin/lib/presets.mjs +59 -73
- package/bin/lib/product-copy.mjs +1 -6
- package/bin/lib/project-root.mjs +31 -62
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +405 -117
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +247 -9
- package/bin/lib/scan-files.mjs +36 -4
- package/bin/lib/schema-validation.mjs +130 -0
- package/bin/lib/sensor-promote-cli.mjs +2 -11
- package/bin/lib/sensor-promote-io.mjs +33 -22
- package/bin/lib/sensor-promotion.mjs +1 -1
- package/bin/lib/skill-catalog-apply.mjs +1 -1
- package/bin/lib/skill-install.mjs +42 -38
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/start-install-recovery.mjs +72 -2
- package/bin/lib/start-preview.mjs +31 -1
- package/bin/lib/states-transitions-presence.mjs +3 -3
- package/bin/lib/status-command.mjs +35 -22
- package/bin/lib/status-manifest.mjs +50 -30
- package/bin/lib/status-transition-catalog.mjs +0 -6
- package/bin/lib/suggestions.mjs +3 -3
- package/bin/lib/team-parliament-io.mjs +76 -88
- package/bin/lib/team-parliament.mjs +21 -10
- package/bin/lib/upgrade-behavior-changes.mjs +2 -2
- package/bin/lib/upgrade-command.mjs +2 -24
- package/bin/lib/upgrade-outcome.mjs +1 -1
- package/bin/lib/upstream-report.mjs +21 -10
- package/bin/lib/violations.mjs +10 -9
- package/bin/lib/weakest-link.mjs +13 -0
- package/bin/lib/write-path-capabilities.mjs +6 -4
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-AHACEKSV.js +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/chunk-UN5EH4QV.cjs +16 -0
- package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
- package/dist/configTypes-GPvvdleM.d.ts +257 -0
- package/dist/eslint/index.cjs +18 -5
- package/dist/eslint/index.d.cts +368 -0
- package/dist/eslint/index.d.ts +139 -156
- package/dist/eslint/index.js +11 -5
- package/dist/index.cjs +36 -35
- package/dist/index.d.cts +5547 -0
- package/dist/index.d.ts +3283 -198
- package/dist/index.js +36 -35
- package/dist/inspectorListen-6DQVJ4DC.js +1 -0
- package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
- package/dist/nestjs/index.cjs +1 -18
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +1 -3
- package/dist/nestjs/index.js +1 -18
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.cts +155 -0
- package/dist/order/index.d.ts +15 -3
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +13 -30
- package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
- package/dist/runtime/index.d.ts +2741 -133
- package/dist/runtime/index.js +13 -30
- package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
- package/dist/types-CnauMQQL.d.ts +1800 -0
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +217 -16
- package/docs/ai-gates.md +174 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +230 -31
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +212 -19
- package/docs/package-surface.md +35 -9
- package/docs/product-voice.md +7 -1
- package/package.json +69 -19
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +77 -12
- package/schemas/ark.invariant-probe.schema.json +368 -0
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +22 -0
- package/server.json +3 -9
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
- package/templates/agent-skills/ark-contract/SKILL.md +17 -0
- package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
- package/templates/agent-skills/ark-explore/SKILL.md +7 -0
- package/templates/agent-skills/ark-fix/SKILL.md +20 -0
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-place/SKILL.md +8 -0
- package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
- package/templates/arkrules/PersistenceAdapters.json +1 -1
- package/templates/arkrules/PresentationAdapters.json +1 -1
- package/templates/skills/ark-adopt.md +5 -1
- package/templates/skills/ark-contract.md +17 -0
- package/templates/skills/ark-coverage.md +2 -0
- package/templates/skills/ark-explore.md +7 -0
- package/templates/skills/ark-fix.md +20 -0
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-place.md +8 -0
- package/templates/skills/ark-runtime.md +6 -1
- package/bin/lib/ark-order-error.mjs +0 -18
- package/bin/lib/ark-order-invariants.mjs +0 -346
- package/dist/types-Ik_OakcI.d.ts +0 -183
package/bin/ark-mcp-runtime.mjs
CHANGED
|
@@ -28,12 +28,11 @@ import { createImportTargetResolver } from './lib/import-resolve.mjs';
|
|
|
28
28
|
import { validateWithAutoPatch, resolveImportFileAbs } from './lib/auto-patch.mjs';
|
|
29
29
|
import { composePrepareWrite } from './lib/prepare-write.mjs';
|
|
30
30
|
import { placementDescriptionFields } from './lib/layer-description.mjs';
|
|
31
|
+
import { sliceRuleIdsForFile } from './ark-layer-match.mjs';
|
|
31
32
|
import { loadArkConfigContract } from './lib/config-contract.mjs';
|
|
32
33
|
import { loadEffectiveArkRulesFromDisk } from './lib/effective-contract-load.mjs';
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
inventoryToExtractionCard,
|
|
36
|
-
} from './lib/rules-inventory.mjs';
|
|
34
|
+
import { loadArkRulesContract } from './lib/arkrules-contract.mjs';
|
|
35
|
+
import { buildRulesInventoryPayload } from './lib/rules-under-contract.mjs';
|
|
37
36
|
import { ARK_ANALYSIS_RESULT_SCHEMA, createAdapterResult } from './lib/adapter-contract.mjs';
|
|
38
37
|
import {
|
|
39
38
|
ARK_PROJECT_IDENTITY_SCHEMA,
|
|
@@ -63,8 +62,18 @@ function arkRulesCatalogForManifest(snapshot) {
|
|
|
63
62
|
sourceFile: r.provenance?.sourceFile,
|
|
64
63
|
coverage: r.coverage,
|
|
65
64
|
}));
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
// ADR 0012 D2: an unreferenced arkrules/*.json is visible drift (advisory).
|
|
66
|
+
const unreferencedFiles = (snapshot.warnings ?? []).map((w) => w.path).filter(Boolean);
|
|
67
|
+
if (structure.length === 0 && invariants.length === 0 && unreferencedFiles.length === 0) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
arkRulesCatalog: {
|
|
72
|
+
structure,
|
|
73
|
+
invariants,
|
|
74
|
+
...(unreferencedFiles.length > 0 ? { unreferencedFiles } : {}),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
68
77
|
}
|
|
69
78
|
import { loadTypeScript } from './lib/typescript-host.mjs';
|
|
70
79
|
import { validateSnippetAnalysis } from './lib/snippet-analysis.mjs';
|
|
@@ -86,6 +95,8 @@ import {
|
|
|
86
95
|
formatWriteGateDeny,
|
|
87
96
|
unclassifiedIncludedWriteDeny,
|
|
88
97
|
requiredOwnerWriteDeny,
|
|
98
|
+
relativeToHookRoot,
|
|
99
|
+
canonicalPathLoose,
|
|
89
100
|
} from './lib/mcp-hook-payload.mjs';
|
|
90
101
|
import {
|
|
91
102
|
canonicalizeCandidateChanges,
|
|
@@ -102,16 +113,38 @@ import {
|
|
|
102
113
|
startResidentHookServer,
|
|
103
114
|
} from './lib/resident-hook.mjs';
|
|
104
115
|
import { resolveArchitectureSnapshot } from './lib/architecture-scan.mjs';
|
|
116
|
+
import { projectImporterIndex } from './lib/import-graph-projection.mjs';
|
|
105
117
|
import { runDoctor } from './lib/doctor-plan.mjs';
|
|
106
118
|
import {
|
|
107
119
|
evaluateWriteDesignDelta,
|
|
108
120
|
formatDesignDeltaBlock,
|
|
109
121
|
} from './lib/design-delta.mjs';
|
|
110
122
|
import { attachPolicyAdrNote } from './lib/adr-path.mjs';
|
|
123
|
+
import {
|
|
124
|
+
candidateArkRulesMatchProject,
|
|
125
|
+
resolvePolicyDeltaArkRules,
|
|
126
|
+
} from './lib/policy-delta-io.mjs';
|
|
111
127
|
|
|
112
128
|
const arkCheckBin = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
|
|
113
129
|
const arkMcpLauncher = fileURLToPath(new URL('./ark-mcp.mjs', import.meta.url));
|
|
114
130
|
|
|
131
|
+
/** MCP tool result for a thrown error: its message as text, flagged isError. */
|
|
132
|
+
function toolErrorResult(error) {
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
135
|
+
isError: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** MCP tool result for a `{ valid }` verdict: pretty JSON text + structuredContent. */
|
|
140
|
+
function validityToolResult(result) {
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
143
|
+
structuredContent: result,
|
|
144
|
+
isError: !result.valid,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
115
148
|
/**
|
|
116
149
|
* W4 — opt-in hook repair payload.
|
|
117
150
|
* True when CLI `--hook-repair` or env ARK_HOOK_REPAIR is 1/true/yes.
|
|
@@ -300,8 +333,10 @@ function isResolvedAnalysisInput(relativePath, args, compilerInputs = new Set())
|
|
|
300
333
|
const relative = String(relativePath).replace(/\\/g, '/');
|
|
301
334
|
try {
|
|
302
335
|
const candidate = resolvedInputIdentities(args.root, [relative]);
|
|
336
|
+
// Referenced ArkRules files are contract inputs too: a patch that edits one cannot
|
|
337
|
+
// be preflighted against the catalog loaded from disk.
|
|
303
338
|
const known = resolvedInputIdentities(args.root, [
|
|
304
|
-
args.config, args.manifest, args.tsconfig, ...compilerInputs,
|
|
339
|
+
args.config, args.manifest, args.tsconfig, ...compilerInputs, ...(args.arkRulesInputs ?? []),
|
|
305
340
|
]);
|
|
306
341
|
if ([...candidate].some((identity) => known.has(identity))) return true;
|
|
307
342
|
} catch {
|
|
@@ -345,24 +380,143 @@ function extraMergeTeethClassification(root, config) {
|
|
|
345
380
|
};
|
|
346
381
|
}
|
|
347
382
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Load the Effective ArkRules the same way ark-check does. Errors are kept (never
|
|
385
|
+
* thrown) so each surface decides: write paths fail closed, manifest/inventory degrade.
|
|
386
|
+
*/
|
|
387
|
+
function loadArkRulesSnapshot(root, config) {
|
|
388
|
+
try {
|
|
389
|
+
const loaded = loadEffectiveArkRulesFromDisk(root, config);
|
|
390
|
+
return {
|
|
391
|
+
arkRules: loaded.arkRules ?? null,
|
|
392
|
+
warnings: loaded.warnings ?? [],
|
|
393
|
+
errors: loaded.errors ?? [],
|
|
394
|
+
};
|
|
395
|
+
} catch (error) {
|
|
396
|
+
return {
|
|
397
|
+
arkRules: null,
|
|
398
|
+
warnings: [],
|
|
399
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Single-file context for validateSnippetAnalysis: layer plane plus every extra plane
|
|
406
|
+
* CI enforces on the same file (ArkRun, ArkRules structure, ArkOrder), with the same
|
|
407
|
+
* classification floor. `surface` only tunes guidance text (hook vs MCP).
|
|
408
|
+
*/
|
|
409
|
+
function snippetContext({
|
|
410
|
+
root,
|
|
411
|
+
config,
|
|
412
|
+
filePath,
|
|
413
|
+
layer,
|
|
414
|
+
relFile,
|
|
415
|
+
classification,
|
|
416
|
+
arkRulesSnapshot,
|
|
417
|
+
surface,
|
|
418
|
+
}) {
|
|
419
|
+
const base = { layer, filePath, ...(surface ? { surface } : {}) };
|
|
420
|
+
const arkRulesOn = (arkRulesSnapshot?.arkRules?.structure?.length ?? 0) > 0;
|
|
421
|
+
const contractErrors = arkRulesSnapshot?.errors ?? [];
|
|
422
|
+
if (!config?.arkRun && !config?.arkOrder && !arkRulesOn && contractErrors.length === 0) {
|
|
423
|
+
return base;
|
|
424
|
+
}
|
|
351
425
|
const relative =
|
|
352
426
|
relFile ||
|
|
353
427
|
(typeof filePath === 'string'
|
|
354
428
|
? path.relative(root, path.resolve(root, filePath)).split(path.sep).join('/')
|
|
355
429
|
: undefined);
|
|
430
|
+
const inRoot =
|
|
431
|
+
typeof relative === 'string' &&
|
|
432
|
+
relative !== '' &&
|
|
433
|
+
relative !== '..' &&
|
|
434
|
+
!relative.startsWith('../') &&
|
|
435
|
+
!path.isAbsolute(relative);
|
|
356
436
|
return {
|
|
357
|
-
|
|
358
|
-
filePath,
|
|
437
|
+
...base,
|
|
359
438
|
relFile: relative,
|
|
360
|
-
|
|
439
|
+
inScope: inRoot ? isCandidateSourceInScope(config, relative) : false,
|
|
440
|
+
...(config.arkRun ? { arkRun: config.arkRun } : {}),
|
|
441
|
+
...(config.arkOrder ? { arkOrder: config.arkOrder } : {}),
|
|
442
|
+
...(arkRulesOn ? { arkRules: arkRulesSnapshot.arkRules } : {}),
|
|
443
|
+
...(contractErrors.length > 0 ? { contractErrors } : {}),
|
|
361
444
|
layers: config.layers ?? [],
|
|
362
445
|
classification: classification ?? extraMergeTeethClassification(root, config),
|
|
363
446
|
};
|
|
364
447
|
}
|
|
365
448
|
|
|
449
|
+
function configuredConfigPath(args) {
|
|
450
|
+
return path.isAbsolute(args.config) ? args.config : path.join(args.root, args.config);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Referenced ArkRules files (config.arkRules: layer -> project-relative path), keyed by
|
|
455
|
+
* canonical absolute path. These are law files exactly like ark.config.json.
|
|
456
|
+
*/
|
|
457
|
+
function referencedArkRulesFiles(args, config) {
|
|
458
|
+
const refs = config?.arkRules;
|
|
459
|
+
const out = new Map();
|
|
460
|
+
if (!refs || typeof refs !== 'object') return out;
|
|
461
|
+
for (const [layer, rel] of Object.entries(refs)) {
|
|
462
|
+
if (typeof rel !== 'string' || rel.length === 0) continue;
|
|
463
|
+
out.set(canonicalPathLoose(path.resolve(args.root, rel)), { layer, rel });
|
|
464
|
+
}
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function lawFileLabel(args, absolute) {
|
|
469
|
+
return (relativeToHookRoot(args.root, absolute) ?? path.basename(absolute))
|
|
470
|
+
.split(path.sep)
|
|
471
|
+
.join('/');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Law-file guard: a write that replaces the configured ark.config.json, or an ArkRules
|
|
476
|
+
* file it references, with content the contract loader rejects (or deletes it) would
|
|
477
|
+
* switch enforcement off for every later write. Deny it up front with the loader's
|
|
478
|
+
* reason. `content: null` / `delete: true` is a deletion.
|
|
479
|
+
*/
|
|
480
|
+
function lawFileWriteDeny(args, config, writes) {
|
|
481
|
+
const configPath = canonicalPathLoose(configuredConfigPath(args));
|
|
482
|
+
const arkRulesFiles = referencedArkRulesFiles(args, config);
|
|
483
|
+
for (const write of writes) {
|
|
484
|
+
if (typeof write?.filePath !== 'string') continue;
|
|
485
|
+
const target = canonicalPathLoose(path.resolve(args.root, write.filePath));
|
|
486
|
+
const isConfig = target === configPath;
|
|
487
|
+
const arkRulesRef = isConfig ? undefined : arkRulesFiles.get(target);
|
|
488
|
+
if (!isConfig && !arkRulesRef) continue;
|
|
489
|
+
const deleting = write.delete === true || write.content === null;
|
|
490
|
+
if (!deleting && typeof write.content !== 'string') continue;
|
|
491
|
+
let reason = null;
|
|
492
|
+
if (deleting) {
|
|
493
|
+
reason = 'the file would be deleted';
|
|
494
|
+
} else {
|
|
495
|
+
try {
|
|
496
|
+
const parsed = JSON.parse(write.content);
|
|
497
|
+
if (isConfig) loadArkConfigContract(parsed, configPath);
|
|
498
|
+
else loadArkRulesContract(parsed, arkRulesRef.rel, arkRulesRef.layer);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
reason = error instanceof Error ? error.message : String(error);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (reason === null) continue;
|
|
504
|
+
const file = isConfig ? lawFileLabel(args, configPath) : arkRulesRef.rel;
|
|
505
|
+
return {
|
|
506
|
+
file,
|
|
507
|
+
message: formatWriteGateDeny({
|
|
508
|
+
file,
|
|
509
|
+
reason: `this write would leave ${file} unloadable, which switches the write gate off: ${reason}`,
|
|
510
|
+
ruleId: 'WRITE_GATE_UNAVAILABLE',
|
|
511
|
+
nextAction: isConfig
|
|
512
|
+
? 'Write a complete, valid contract (same schema ark-check --strict-config reads); do not remove or empty ark.config.json.'
|
|
513
|
+
: `Write a complete, valid ArkRules file for layer ${arkRulesRef.layer} (same schema ark-check loads); to drop it, remove its arkRules reference from ark.config.json in a law change.`,
|
|
514
|
+
}),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
|
|
366
520
|
function designDeltaViolations(delta) {
|
|
367
521
|
return (delta?.changes ?? []).map((change) => ({
|
|
368
522
|
ruleId: 'DESIGN_SMELL_REGRESSION',
|
|
@@ -418,10 +572,30 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
418
572
|
// Codex ApplyPatch is only preflighted when Ark can reconstruct every file operation.
|
|
419
573
|
// An incomplete reconstruction must not be mislabeled as atomic or hard enforcement.
|
|
420
574
|
if (!parsedPatch.complete) {
|
|
575
|
+
// Documented residual: a patch Ark cannot reconstruct stays CI-backed. Say so
|
|
576
|
+
// instead of allowing silently.
|
|
577
|
+
output.stderr(
|
|
578
|
+
'Ark architecture gate: this apply_patch could not be fully reconstructed, so it was ' +
|
|
579
|
+
'not preflighted here; CI (--strict-merge) still checks it.\n'
|
|
580
|
+
);
|
|
421
581
|
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
422
582
|
return;
|
|
423
583
|
}
|
|
424
584
|
const patchWrites = parsedPatch.writes;
|
|
585
|
+
const lawDeny = lawFileWriteDeny(
|
|
586
|
+
args,
|
|
587
|
+
config,
|
|
588
|
+
patchWrites.map((change) => ({
|
|
589
|
+
filePath: change.filePath,
|
|
590
|
+
content: change.delete === true ? null : change.content,
|
|
591
|
+
delete: change.delete === true,
|
|
592
|
+
}))
|
|
593
|
+
);
|
|
594
|
+
if (lawDeny) {
|
|
595
|
+
emitHostDeny(output, { antigravityStyle, cursorStyle, grokStyle, ...lawDeny });
|
|
596
|
+
output.status(2);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
425
599
|
const patchChangeSet = classifyChangeSet(patchWrites.map((change) => String(change.path)));
|
|
426
600
|
const patchLawGate = evaluateTeamGate({
|
|
427
601
|
changeSet: patchChangeSet,
|
|
@@ -608,14 +782,17 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
608
782
|
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
609
783
|
return;
|
|
610
784
|
}
|
|
611
|
-
|
|
785
|
+
// Pre-existing (ratcheted) findings ride as warnings; the deny names what blocks.
|
|
786
|
+
const blocking = result.diagnostics.filter((d) => d.severity === 'error');
|
|
787
|
+
const shown = blocking.length > 0 ? blocking : result.diagnostics;
|
|
788
|
+
const first = shown[0];
|
|
612
789
|
const message = formatWriteGateDeny({
|
|
613
790
|
file: `${changes.length} file(s)`,
|
|
614
791
|
reason: first?.message || `this ${toolName} breaks the architecture layers`,
|
|
615
792
|
ruleId: first?.ruleId,
|
|
616
793
|
nextAction: first?.nextAction,
|
|
617
794
|
extraLines: [
|
|
618
|
-
...
|
|
795
|
+
...shown.slice(1).map((d) => `[${d.ruleId}] ${d.message}`),
|
|
619
796
|
...(designDelta && !designDelta.valid
|
|
620
797
|
? formatDesignDeltaBlock(designDelta).split('\n').slice(1)
|
|
621
798
|
: []),
|
|
@@ -639,19 +816,46 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
639
816
|
output.status(2);
|
|
640
817
|
return;
|
|
641
818
|
}
|
|
642
|
-
const
|
|
819
|
+
const rawFilePath = toolInput.file_path;
|
|
643
820
|
if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) {
|
|
644
821
|
// Non-file tools: fail-open. Antigravity still needs an explicit allow decision.
|
|
645
822
|
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
646
823
|
return;
|
|
647
824
|
}
|
|
648
|
-
if (typeof
|
|
825
|
+
if (typeof rawFilePath === 'string' && !SOURCE_FILE.test(rawFilePath)) {
|
|
826
|
+
// Law files are not source, but a write that leaves ark.config.json or a referenced
|
|
827
|
+
// ArkRules file unloadable would silently switch the gate off for every later write.
|
|
828
|
+
const source = proposedSource(toolName, toolInput);
|
|
829
|
+
const lawDeny =
|
|
830
|
+
typeof source === 'string'
|
|
831
|
+
? lawFileWriteDeny(args, config, [{ filePath: rawFilePath, content: source }])
|
|
832
|
+
: null;
|
|
833
|
+
if (lawDeny) {
|
|
834
|
+
emitHostDeny(output, { antigravityStyle, cursorStyle, grokStyle, ...lawDeny });
|
|
835
|
+
output.status(2);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (
|
|
840
|
+
typeof rawFilePath !== 'string' ||
|
|
841
|
+
!SOURCE_FILE.test(rawFilePath) ||
|
|
842
|
+
rawFilePath.endsWith('.d.ts')
|
|
843
|
+
) {
|
|
649
844
|
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
650
845
|
return;
|
|
651
846
|
}
|
|
652
|
-
|
|
847
|
+
// Containment compares the caller's spelling first, then the canonical (realpath)
|
|
848
|
+
// spelling: /tmp vs /private/tmp or a symlinked checkout is the same workspace.
|
|
849
|
+
// Every downstream consumer then sees the path re-spelled under args.root.
|
|
850
|
+
const rootRelative = relativeToHookRoot(args.root, path.resolve(rawFilePath));
|
|
851
|
+
if (rootRelative === null) {
|
|
852
|
+
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const rel = rootRelative;
|
|
856
|
+
const filePath = path.join(args.root, rel);
|
|
653
857
|
const segments = rel.split(path.sep);
|
|
654
|
-
if (segments
|
|
858
|
+
if (segments.includes('node_modules')) {
|
|
655
859
|
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
656
860
|
return;
|
|
657
861
|
}
|
|
@@ -708,19 +912,17 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
708
912
|
output.status(2);
|
|
709
913
|
return;
|
|
710
914
|
}
|
|
915
|
+
const context = snippetContext({
|
|
916
|
+
root: args.root,
|
|
917
|
+
config,
|
|
918
|
+
filePath,
|
|
919
|
+
layer,
|
|
920
|
+
relFile: normalizedRel,
|
|
921
|
+
arkRulesSnapshot: args.arkRulesSnapshot,
|
|
922
|
+
surface: 'hook',
|
|
923
|
+
});
|
|
711
924
|
const validateOnce = (src) =>
|
|
712
|
-
validateSnippetAnalysis({
|
|
713
|
-
gate,
|
|
714
|
-
ts,
|
|
715
|
-
source: src,
|
|
716
|
-
context: arkRunSnippetContext({
|
|
717
|
-
root: args.root,
|
|
718
|
-
config,
|
|
719
|
-
filePath,
|
|
720
|
-
layer,
|
|
721
|
-
relFile: normalizedRel,
|
|
722
|
-
}),
|
|
723
|
-
});
|
|
925
|
+
validateSnippetAnalysis({ gate, ts, source: src, context });
|
|
724
926
|
// W1: one validation pass (+ optional autoPatch). Original write still blocked when
|
|
725
927
|
// invalid; hosts must apply autoPatch explicitly (never silent write).
|
|
726
928
|
const result = ts
|
|
@@ -761,12 +963,18 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
761
963
|
// every subsequent edit to that file un-writable while CI passes. Same-file keys ignore
|
|
762
964
|
// line numbers (edits shift them); simpler than full baselineKey (no file/layer fields
|
|
763
965
|
// needed — this file is fixed).
|
|
966
|
+
// Contract-level failures (WRITE_GATE_UNAVAILABLE: a referenced ArkRules file missing
|
|
967
|
+
// or invalid) say nothing about this file's content. The on-disk version reports the
|
|
968
|
+
// same one, so ratcheting it would cancel it and let every edit to an existing file
|
|
969
|
+
// skip the sensors that could not load. They are never pre-existing: always deny.
|
|
970
|
+
const ratchetExempt = (rule) =>
|
|
971
|
+
rule.startsWith('ANALYSIS_') || rule === 'WRITE_GATE_UNAVAILABLE';
|
|
764
972
|
const violationKey = (violation) => `${violation.ruleId}|${violation.target ?? violation.message}`;
|
|
765
973
|
let existingCounts = new Map();
|
|
766
974
|
try {
|
|
767
975
|
const current = fs.readFileSync(filePath, 'utf8');
|
|
768
976
|
for (const violation of validateOnce(current).violations) {
|
|
769
|
-
if (String(violation.ruleId ?? violation.code)
|
|
977
|
+
if (ratchetExempt(String(violation.ruleId ?? violation.code))) continue;
|
|
770
978
|
const key = violationKey(violation);
|
|
771
979
|
existingCounts.set(key, (existingCounts.get(key) ?? 0) + 1);
|
|
772
980
|
}
|
|
@@ -777,7 +985,7 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
777
985
|
const rule = String(violation.ruleId ?? violation.code);
|
|
778
986
|
// Incremental mid-edit parse errors are normal for agents — do not deny solely on them.
|
|
779
987
|
if (rule === 'ANALYSIS_PARSE_INCOMPLETE') return false;
|
|
780
|
-
if (rule
|
|
988
|
+
if (ratchetExempt(rule)) return true;
|
|
781
989
|
const key = violationKey(violation);
|
|
782
990
|
const remaining = existingCounts.get(key) ?? 0;
|
|
783
991
|
if (remaining === 0) return true;
|
|
@@ -961,7 +1169,7 @@ function residentCompilerInputs(ts, args) {
|
|
|
961
1169
|
return [...inputs];
|
|
962
1170
|
}
|
|
963
1171
|
|
|
964
|
-
function residentHookInputs(ts, args) {
|
|
1172
|
+
function residentHookInputs(ts, args, config) {
|
|
965
1173
|
const configPath = path.isAbsolute(args.config)
|
|
966
1174
|
? args.config
|
|
967
1175
|
: path.join(args.root, args.config);
|
|
@@ -973,6 +1181,7 @@ function residentHookInputs(ts, args) {
|
|
|
973
1181
|
return [
|
|
974
1182
|
configPath,
|
|
975
1183
|
...(manifestPath ? [manifestPath] : []),
|
|
1184
|
+
...arkRulesInputPaths(args.root, config),
|
|
976
1185
|
...residentCompilerInputs(ts, args),
|
|
977
1186
|
...[
|
|
978
1187
|
'package.json',
|
|
@@ -985,6 +1194,15 @@ function residentHookInputs(ts, args) {
|
|
|
985
1194
|
];
|
|
986
1195
|
}
|
|
987
1196
|
|
|
1197
|
+
/** Referenced ArkRules files (existing or not): editing/deleting one invalidates caches. */
|
|
1198
|
+
function arkRulesInputPaths(root, config) {
|
|
1199
|
+
const refs = config?.arkRules;
|
|
1200
|
+
if (!refs || typeof refs !== 'object') return [];
|
|
1201
|
+
return Object.values(refs)
|
|
1202
|
+
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
1203
|
+
.map((value) => path.resolve(root, value));
|
|
1204
|
+
}
|
|
1205
|
+
|
|
988
1206
|
function captureResidentHook(payload, gate, config, args, ts, request) {
|
|
989
1207
|
let stdout = '';
|
|
990
1208
|
let stderr = '';
|
|
@@ -1067,7 +1285,9 @@ function createResidentDoctorSession(args, config, ts) {
|
|
|
1067
1285
|
const resolutionInputs = snapshotInputPaths(snapshot.inputs);
|
|
1068
1286
|
const ledger = createResidentInputLedger([...after.paths, ...resolutionInputs]);
|
|
1069
1287
|
if (!ledger.matches([...after.paths, ...resolutionInputs])) return null;
|
|
1070
|
-
|
|
1288
|
+
// Same importer index the cold doctor projects (ADR 0037), built once per snapshot.
|
|
1289
|
+
const importGraph = projectImporterIndex(snapshot.facts);
|
|
1290
|
+
return { root: args.root, files: after.files, ledger, resolutionInputs, rules, snapshot, importGraph };
|
|
1071
1291
|
}
|
|
1072
1292
|
|
|
1073
1293
|
function verifyResidentDoctorSession(session) {
|
|
@@ -1088,6 +1308,7 @@ function renderResidentDoctor(session, args, config, ts) {
|
|
|
1088
1308
|
ts,
|
|
1089
1309
|
parseHealth: session.snapshot.result.parseHealth,
|
|
1090
1310
|
completeness: session.snapshot.result.completeness,
|
|
1311
|
+
architectureFacts: { importGraph: session.importGraph },
|
|
1091
1312
|
writeJson: (value) => {
|
|
1092
1313
|
stdout += `${value}\n`;
|
|
1093
1314
|
},
|
|
@@ -1121,7 +1342,7 @@ async function startResidentHookControl({ args, gate, config, ts, loadedTypeScri
|
|
|
1121
1342
|
launcher: arkMcpLauncher,
|
|
1122
1343
|
});
|
|
1123
1344
|
const identityPaths = [
|
|
1124
|
-
...residentHookInputs(ts, args),
|
|
1345
|
+
...residentHookInputs(ts, args, config),
|
|
1125
1346
|
...(loadedTypeScript?.resolvedPath ? [loadedTypeScript.resolvedPath] : []),
|
|
1126
1347
|
];
|
|
1127
1348
|
const identityTokens = [
|
|
@@ -1289,10 +1510,12 @@ function printSessionContext(config, profile, forbiddenGlobals, args, configPath
|
|
|
1289
1510
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
1290
1511
|
}
|
|
1291
1512
|
|
|
1292
|
-
export async function runArkMcp({ hookInput } = {}) {
|
|
1513
|
+
export async function runArkMcp({ hookInput, hookRoot } = {}) {
|
|
1293
1514
|
const processStartedAt = new Date().toISOString();
|
|
1294
1515
|
const runtimeId = randomUUID();
|
|
1295
1516
|
const args = parseArgs(process.argv);
|
|
1517
|
+
// The launcher already resolved the hook's project root (walk-up from the payload).
|
|
1518
|
+
if (args.hook && typeof hookRoot === 'string' && hookRoot) args.root = hookRoot;
|
|
1296
1519
|
const requestedRoot = args.root;
|
|
1297
1520
|
const resolvedRoot = canonicalPathIncludingMissing(requestedRoot);
|
|
1298
1521
|
// MCP identity and file containment use the canonical workspace. Hook payloads,
|
|
@@ -1437,6 +1660,12 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1437
1660
|
},
|
|
1438
1661
|
});
|
|
1439
1662
|
|
|
1663
|
+
// Effective ArkRules load once, before the hook: write paths run the same
|
|
1664
|
+
// structure sensors CI runs and fail closed when a referenced file is broken.
|
|
1665
|
+
const effectiveArkRulesSnapshot = loadArkRulesSnapshot(args.root, config);
|
|
1666
|
+
args.arkRulesSnapshot = effectiveArkRulesSnapshot;
|
|
1667
|
+
args.arkRulesInputs = arkRulesInputPaths(args.root, config);
|
|
1668
|
+
|
|
1440
1669
|
if (args.hook) {
|
|
1441
1670
|
runHook(gate, config, args, ts, hookInput);
|
|
1442
1671
|
return;
|
|
@@ -1447,23 +1676,6 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1447
1676
|
return;
|
|
1448
1677
|
}
|
|
1449
1678
|
|
|
1450
|
-
const effectiveArkRulesSnapshot = (() => {
|
|
1451
|
-
try {
|
|
1452
|
-
const loaded = loadEffectiveArkRulesFromDisk(args.root, config);
|
|
1453
|
-
return {
|
|
1454
|
-
arkRules: loaded.arkRules ?? null,
|
|
1455
|
-
warnings: loaded.warnings ?? [],
|
|
1456
|
-
errors: loaded.errors ?? [],
|
|
1457
|
-
};
|
|
1458
|
-
} catch (error) {
|
|
1459
|
-
return {
|
|
1460
|
-
arkRules: null,
|
|
1461
|
-
warnings: [],
|
|
1462
|
-
errors: [error instanceof Error ? error.message : String(error)],
|
|
1463
|
-
};
|
|
1464
|
-
}
|
|
1465
|
-
})();
|
|
1466
|
-
|
|
1467
1679
|
const residentHookControl = await startResidentHookControl({
|
|
1468
1680
|
args,
|
|
1469
1681
|
gate,
|
|
@@ -1474,6 +1686,13 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1474
1686
|
});
|
|
1475
1687
|
if (residentHookControl) process.once('exit', residentHookControl.cleanup);
|
|
1476
1688
|
|
|
1689
|
+
const contractInputPaths = [
|
|
1690
|
+
configPath,
|
|
1691
|
+
...(manifestPath ? [manifestPath] : []),
|
|
1692
|
+
...arkRulesInputPaths(args.root, config),
|
|
1693
|
+
].filter(Boolean);
|
|
1694
|
+
const startupContractIdentity = residentEnvironmentIdentity(contractInputPaths);
|
|
1695
|
+
|
|
1477
1696
|
const SERVER_INFO = { name: 'arkgate', version: ark.version };
|
|
1478
1697
|
const DEFAULT_PROTOCOL = '2024-11-05';
|
|
1479
1698
|
const projectIdentity = createProjectIdentity({
|
|
@@ -1569,6 +1788,31 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1569
1788
|
overallOk: { type: 'boolean' },
|
|
1570
1789
|
},
|
|
1571
1790
|
};
|
|
1791
|
+
// withProjectContext always spreads processPackage (FX06) into structuredContent;
|
|
1792
|
+
// both oneOf branches are additionalProperties:false, so it must be declared.
|
|
1793
|
+
const processPackageOutputSchema = {
|
|
1794
|
+
type: 'object',
|
|
1795
|
+
additionalProperties: false,
|
|
1796
|
+
required: [
|
|
1797
|
+
'schemaVersion',
|
|
1798
|
+
'notAScore',
|
|
1799
|
+
'processArkgateVersion',
|
|
1800
|
+
'projectInstalledVersion',
|
|
1801
|
+
'processPackageMismatch',
|
|
1802
|
+
'processStale',
|
|
1803
|
+
'nextAction',
|
|
1804
|
+
],
|
|
1805
|
+
properties: {
|
|
1806
|
+
schemaVersion: { const: '1.0' },
|
|
1807
|
+
notAScore: { const: true },
|
|
1808
|
+
processArkgateVersion: { type: ['string', 'null'] },
|
|
1809
|
+
projectInstalledVersion: { type: ['string', 'null'] },
|
|
1810
|
+
processPackageMismatch: { type: 'boolean' },
|
|
1811
|
+
processStale: { type: 'boolean' },
|
|
1812
|
+
nextAction: { type: 'string', minLength: 1 },
|
|
1813
|
+
},
|
|
1814
|
+
};
|
|
1815
|
+
const contractStaleOutputSchema = { type: 'boolean' };
|
|
1572
1816
|
const analysisResultWithProjectSchema = {
|
|
1573
1817
|
type: ARK_ANALYSIS_RESULT_SCHEMA.type,
|
|
1574
1818
|
additionalProperties: ARK_ANALYSIS_RESULT_SCHEMA.additionalProperties,
|
|
@@ -1578,19 +1822,23 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1578
1822
|
'projectIdentity',
|
|
1579
1823
|
'binding',
|
|
1580
1824
|
'authoritative',
|
|
1825
|
+
'processPackage',
|
|
1581
1826
|
],
|
|
1582
1827
|
properties: {
|
|
1583
1828
|
...ARK_ANALYSIS_RESULT_SCHEMA.properties,
|
|
1584
1829
|
projectIdentity: projectIdentityOutputSchema,
|
|
1585
1830
|
binding: PROJECT_BINDING_SCHEMA,
|
|
1586
1831
|
authoritative: { type: 'boolean' },
|
|
1832
|
+
processPackage: processPackageOutputSchema,
|
|
1833
|
+
contractStale: contractStaleOutputSchema,
|
|
1834
|
+
lexicalValid: { type: 'boolean' },
|
|
1587
1835
|
verdict: checkVerdictOutputSchema,
|
|
1588
1836
|
},
|
|
1589
1837
|
};
|
|
1590
1838
|
const projectBindingErrorSchema = {
|
|
1591
1839
|
type: 'object',
|
|
1592
1840
|
additionalProperties: false,
|
|
1593
|
-
required: ['ok', 'error', 'projectIdentity', 'binding', 'authoritative'],
|
|
1841
|
+
required: ['ok', 'error', 'projectIdentity', 'binding', 'authoritative', 'processPackage'],
|
|
1594
1842
|
properties: {
|
|
1595
1843
|
ok: { const: false },
|
|
1596
1844
|
error: {
|
|
@@ -1605,9 +1853,13 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1605
1853
|
projectIdentity: projectIdentityOutputSchema,
|
|
1606
1854
|
binding: PROJECT_BINDING_SCHEMA,
|
|
1607
1855
|
authoritative: { type: 'boolean' },
|
|
1856
|
+
processPackage: processPackageOutputSchema,
|
|
1857
|
+
contractStale: contractStaleOutputSchema,
|
|
1608
1858
|
},
|
|
1609
1859
|
};
|
|
1860
|
+
// MCP (and the TypeScript SDK) require outputSchema.type === 'object' at the top level.
|
|
1610
1861
|
const projectAwareAnalysisResultSchema = {
|
|
1862
|
+
type: 'object',
|
|
1611
1863
|
oneOf: [analysisResultWithProjectSchema, projectBindingErrorSchema],
|
|
1612
1864
|
};
|
|
1613
1865
|
|
|
@@ -1812,15 +2064,28 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1812
2064
|
});
|
|
1813
2065
|
}
|
|
1814
2066
|
|
|
2067
|
+
/**
|
|
2068
|
+
* Contract inputs (config, manifest, referenced ArkRules) are loaded once at startup.
|
|
2069
|
+
* An edit afterwards makes this process's gate, manifest, and contractHash stale:
|
|
2070
|
+
* report it and fail project tools closed (CONTRACT_STALE) instead of answering
|
|
2071
|
+
* authoritatively from the old contract while ark_check reads the new one.
|
|
2072
|
+
*/
|
|
2073
|
+
function contractStale() {
|
|
2074
|
+
return residentEnvironmentIdentity(contractInputPaths) !== startupContractIdentity;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
1815
2077
|
function contextFor(binding) {
|
|
1816
2078
|
const processPackage = processPackageHonesty();
|
|
2079
|
+
const stale = contractStale();
|
|
1817
2080
|
return {
|
|
1818
2081
|
projectIdentity,
|
|
1819
2082
|
binding,
|
|
1820
2083
|
// A correctly bound project is still non-authoritative when this long-lived
|
|
1821
|
-
// process loaded a different package version than the project
|
|
1822
|
-
|
|
2084
|
+
// process loaded a different package version (or contract) than the project
|
|
2085
|
+
// now has on disk.
|
|
2086
|
+
authoritative: binding.authoritative && !processPackage.processStale && !stale,
|
|
1823
2087
|
processPackage,
|
|
2088
|
+
...(stale ? { contractStale: true } : {}),
|
|
1824
2089
|
};
|
|
1825
2090
|
}
|
|
1826
2091
|
|
|
@@ -1897,6 +2162,31 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1897
2162
|
);
|
|
1898
2163
|
}
|
|
1899
2164
|
|
|
2165
|
+
function staleContractFailureResult(binding) {
|
|
2166
|
+
return withProjectContext(
|
|
2167
|
+
{
|
|
2168
|
+
content: [
|
|
2169
|
+
{
|
|
2170
|
+
type: 'text',
|
|
2171
|
+
text: JSON.stringify({
|
|
2172
|
+
ok: false,
|
|
2173
|
+
error: {
|
|
2174
|
+
code: 'CONTRACT_STALE',
|
|
2175
|
+
message:
|
|
2176
|
+
`ark.config.json, the project manifest, or a referenced ArkRules file changed ` +
|
|
2177
|
+
`since this MCP process started (contractHash ${projectIdentity.contractHash}). ` +
|
|
2178
|
+
'Restart the MCP server, then re-run ark_identity. Until then use the ' +
|
|
2179
|
+
'project-local CLI (npx arkgate-check), which reads the current contract.',
|
|
2180
|
+
},
|
|
2181
|
+
}),
|
|
2182
|
+
},
|
|
2183
|
+
],
|
|
2184
|
+
isError: true,
|
|
2185
|
+
},
|
|
2186
|
+
binding
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
1900
2190
|
function staleProcessFailureResult(binding) {
|
|
1901
2191
|
const processPackage = processPackageHonesty();
|
|
1902
2192
|
return withProjectContext(
|
|
@@ -1941,12 +2231,16 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1941
2231
|
{
|
|
1942
2232
|
name: 'validate_code',
|
|
1943
2233
|
description:
|
|
1944
|
-
"
|
|
1945
|
-
'(forbidden infra imports, unknown intents,
|
|
1946
|
-
'
|
|
1947
|
-
'
|
|
1948
|
-
'
|
|
1949
|
-
'
|
|
2234
|
+
"Single-file lexical check of a source snippet against Ark's architecture " +
|
|
2235
|
+
'(forbidden infra imports, unknown intents, layer-reference violations, and the enforced ' +
|
|
2236
|
+
'ArkRun / ArkRules structure / ArkOrder sensors CI runs on that file). One file can never ' +
|
|
2237
|
+
'prove the whole candidate, so the result is always partial: valid:false and isError:true ' +
|
|
2238
|
+
'until complete-candidate preflight. Read lexicalValid and violations for the snippet ' +
|
|
2239
|
+
'verdict; use ark_prepare_change (or arkgate-check) for a green result. For hard ' +
|
|
2240
|
+
'write-path blocking use the `arkgate-mcp --hook` PreToolUse hook, not this tool. ' +
|
|
2241
|
+
'Returns { valid, lexicalValid, completeness, violations, autoPatch? }. autoPatch (when ' +
|
|
2242
|
+
'present) is a mechanical-safe rewrite of the source (import type conversion); hosts may ' +
|
|
2243
|
+
'apply it instead of re-drafting.',
|
|
1950
2244
|
inputSchema: {
|
|
1951
2245
|
type: 'object',
|
|
1952
2246
|
properties: {
|
|
@@ -2009,6 +2303,20 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2009
2303
|
type: 'object',
|
|
2010
2304
|
description: 'Candidate complete config; defaults to the current project contract.',
|
|
2011
2305
|
},
|
|
2306
|
+
baseArkRuleFiles: {
|
|
2307
|
+
type: 'object',
|
|
2308
|
+
description:
|
|
2309
|
+
'Base ArkRules catalog as data: { "<path from baseConfig.arkRules>": <ArkRules file JSON> }. ' +
|
|
2310
|
+
'Required when baseConfig maps arkRules; otherwise the call is refused rather than ' +
|
|
2311
|
+
'classifying ArkRule demotions/deletions as neutral.',
|
|
2312
|
+
},
|
|
2313
|
+
candidateArkRuleFiles: {
|
|
2314
|
+
type: 'object',
|
|
2315
|
+
description:
|
|
2316
|
+
'Candidate ArkRules catalog as data, same shape. Defaults to the files on disk when ' +
|
|
2317
|
+
'candidateConfig is omitted or maps the same arkRules files as the project contract ' +
|
|
2318
|
+
'(the project ark.config.json passed verbatim counts).',
|
|
2319
|
+
},
|
|
2012
2320
|
acknowledgement: {
|
|
2013
2321
|
type: 'object',
|
|
2014
2322
|
description:
|
|
@@ -2052,12 +2360,13 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2052
2360
|
'What you are building. Does not invent a path — pass filePath. Without filePath the tool fail-closes.',
|
|
2053
2361
|
},
|
|
2054
2362
|
},
|
|
2363
|
+
required: ['filePath'],
|
|
2055
2364
|
},
|
|
2056
2365
|
},
|
|
2057
2366
|
{
|
|
2058
2367
|
name: 'ark_prepare_write',
|
|
2059
2368
|
description:
|
|
2060
|
-
'Prepare a write against the architecture contract: place (filePath
|
|
2369
|
+
'Prepare a write against the architecture contract: place (filePath required; fail-closed without it — never invents a path) + ' +
|
|
2061
2370
|
'constrain (layer, mayImport, mustNotImport, forbiddenGlobals) + validate source + optional ' +
|
|
2062
2371
|
'mechanical-safe autoPatch + judgmentBrief when judgment is needed + contentHash for host commit. ' +
|
|
2063
2372
|
'Also returns the versioned new/worsened designDelta for the proposed full file. ' +
|
|
@@ -2065,7 +2374,8 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2065
2374
|
'When the matched layer has layers[].description, the JSON includes description; the field is omitted when absent. ' +
|
|
2066
2375
|
'When the matched layer has layers[].trustBoundary (public|auth|admin|internal), the JSON includes trustBoundary; omitted when absent. ' +
|
|
2067
2376
|
'When the matched layer has layers[].owners, the JSON includes owners; omitted when absent. ' +
|
|
2068
|
-
'
|
|
2377
|
+
'Single-file evidence is partial, so valid stays false (isError:true) until complete-candidate preflight; read lexicalValid for the snippet verdict and use ark_prepare_change for a green result. ' +
|
|
2378
|
+
'Returns { filePath, layer, description?, trustBoundary?, owners?, valid, lexicalValid, violations?, autoPatch?, judgmentBrief?, contentHash, ... }.',
|
|
2069
2379
|
inputSchema: {
|
|
2070
2380
|
type: 'object',
|
|
2071
2381
|
properties: {
|
|
@@ -2076,14 +2386,15 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2076
2386
|
},
|
|
2077
2387
|
description: {
|
|
2078
2388
|
type: 'string',
|
|
2079
|
-
description:
|
|
2389
|
+
description:
|
|
2390
|
+
'What you are building (hint only). Does not invent a path — pass filePath. Without filePath the tool fail-closes.',
|
|
2080
2391
|
},
|
|
2081
2392
|
layer: {
|
|
2082
2393
|
type: 'string',
|
|
2083
2394
|
description: 'Optional explicit layer override (otherwise inferred from filePath).',
|
|
2084
2395
|
},
|
|
2085
2396
|
},
|
|
2086
|
-
required: ['source'],
|
|
2397
|
+
required: ['source', 'filePath'],
|
|
2087
2398
|
},
|
|
2088
2399
|
},
|
|
2089
2400
|
{
|
|
@@ -2159,6 +2470,8 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2159
2470
|
projectIdentity: projectIdentityOutputSchema,
|
|
2160
2471
|
binding: PROJECT_BINDING_SCHEMA,
|
|
2161
2472
|
authoritative: { type: 'boolean' },
|
|
2473
|
+
processPackage: processPackageOutputSchema,
|
|
2474
|
+
contractStale: contractStaleOutputSchema,
|
|
2162
2475
|
},
|
|
2163
2476
|
},
|
|
2164
2477
|
},
|
|
@@ -2304,18 +2617,15 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2304
2617
|
}
|
|
2305
2618
|
const filePath = params.arguments.filePath;
|
|
2306
2619
|
const layer = params.arguments.layer ?? inferLayer(filePath, config, args.root);
|
|
2307
|
-
const
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
layer,
|
|
2317
|
-
}),
|
|
2318
|
-
});
|
|
2620
|
+
const context = snippetContext({
|
|
2621
|
+
root: args.root,
|
|
2622
|
+
config,
|
|
2623
|
+
filePath,
|
|
2624
|
+
layer,
|
|
2625
|
+
arkRulesSnapshot: effectiveArkRulesSnapshot,
|
|
2626
|
+
surface: 'mcp',
|
|
2627
|
+
});
|
|
2628
|
+
const validateOnce = (src) => validateSnippetAnalysis({ gate, ts, source: src, context });
|
|
2319
2629
|
// W1: attempt mechanical-safe single-file autoPatch (import type), re-validate or discard.
|
|
2320
2630
|
const result = validateWithAutoPatch({
|
|
2321
2631
|
source,
|
|
@@ -2331,6 +2641,12 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2331
2641
|
completenessReasons: result.completenessReasons,
|
|
2332
2642
|
violations: result.violations,
|
|
2333
2643
|
});
|
|
2644
|
+
// lexicalValid is the one-file verdict (valid stays false: single-file evidence
|
|
2645
|
+
// is partial). Same field ark_prepare_write already exposes.
|
|
2646
|
+
const lexicalValid =
|
|
2647
|
+
typeof result.lexicalValid === 'boolean'
|
|
2648
|
+
? result.lexicalValid
|
|
2649
|
+
: (result.violations ?? []).length === 0 && result.completeness !== 'unavailable';
|
|
2334
2650
|
return {
|
|
2335
2651
|
content: [
|
|
2336
2652
|
{
|
|
@@ -2339,6 +2655,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2339
2655
|
{
|
|
2340
2656
|
...adapterResult,
|
|
2341
2657
|
valid: adapterResult.valid,
|
|
2658
|
+
lexicalValid,
|
|
2342
2659
|
violations: result.violations,
|
|
2343
2660
|
...(result.autoPatch ? { autoPatch: result.autoPatch } : {}),
|
|
2344
2661
|
layer,
|
|
@@ -2348,7 +2665,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2348
2665
|
),
|
|
2349
2666
|
},
|
|
2350
2667
|
],
|
|
2351
|
-
structuredContent: adapterResult,
|
|
2668
|
+
structuredContent: { ...adapterResult, lexicalValid },
|
|
2352
2669
|
isError: !adapterResult.valid,
|
|
2353
2670
|
};
|
|
2354
2671
|
}
|
|
@@ -2472,24 +2789,30 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2472
2789
|
}
|
|
2473
2790
|
try {
|
|
2474
2791
|
const acknowledgement = params?.arguments?.acknowledgement;
|
|
2792
|
+
const suppliedCandidate = params?.arguments?.candidateConfig;
|
|
2793
|
+
const candidateConfig = suppliedCandidate ?? config;
|
|
2794
|
+
// ArkRules are part of the policy: classify their transition too, or refuse.
|
|
2795
|
+
const { baseArkRules, candidateArkRules } = resolvePolicyDeltaArkRules({
|
|
2796
|
+
root: args.root,
|
|
2797
|
+
baseConfig,
|
|
2798
|
+
candidateConfig,
|
|
2799
|
+
candidateIsProjectConfig: candidateArkRulesMatchProject(suppliedCandidate, config),
|
|
2800
|
+
baseArkRuleFiles: params?.arguments?.baseArkRuleFiles,
|
|
2801
|
+
candidateArkRuleFiles: params?.arguments?.candidateArkRuleFiles,
|
|
2802
|
+
});
|
|
2475
2803
|
const result = attachPolicyAdrNote(
|
|
2476
2804
|
ark.analyzePolicyDelta({
|
|
2477
2805
|
baseConfig,
|
|
2478
|
-
candidateConfig
|
|
2806
|
+
candidateConfig,
|
|
2479
2807
|
acknowledgement,
|
|
2808
|
+
baseArkRules,
|
|
2809
|
+
candidateArkRules,
|
|
2480
2810
|
}),
|
|
2481
2811
|
{ root: args.root, acknowledgement, failClosed: true }
|
|
2482
2812
|
);
|
|
2483
|
-
return
|
|
2484
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
2485
|
-
structuredContent: result,
|
|
2486
|
-
isError: !result.valid,
|
|
2487
|
-
};
|
|
2813
|
+
return validityToolResult(result);
|
|
2488
2814
|
} catch (error) {
|
|
2489
|
-
return
|
|
2490
|
-
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
2491
|
-
isError: true,
|
|
2492
|
-
};
|
|
2815
|
+
return toolErrorResult(error);
|
|
2493
2816
|
}
|
|
2494
2817
|
}
|
|
2495
2818
|
|
|
@@ -2551,6 +2874,11 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2551
2874
|
rules.some((rule) => !rule.allowed && rule.from === layerName && rule.to === to)
|
|
2552
2875
|
);
|
|
2553
2876
|
const mayImport = otherNames.filter((name) => !mustNotImport.includes(name));
|
|
2877
|
+
const sliceCatalog = [
|
|
2878
|
+
...(args.arkRulesSnapshot?.arkRules?.structure ?? []),
|
|
2879
|
+
...(args.arkRulesSnapshot?.arkRules?.invariants ?? []),
|
|
2880
|
+
].map((row) => ({ id: row.id, childId: row.provenance?.childId }));
|
|
2881
|
+
const sliceRules = sliceRuleIdsForFile(filePath, rules, sliceCatalog);
|
|
2554
2882
|
return withGolden({
|
|
2555
2883
|
filePath,
|
|
2556
2884
|
layer: layerName,
|
|
@@ -2558,6 +2886,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2558
2886
|
...placementDescriptionFields(layerMeta),
|
|
2559
2887
|
forbiddenGlobals: layerMeta?.forbiddenGlobals ?? [],
|
|
2560
2888
|
...(layerMeta?.mayImportInfrastructure ? { mayImportInfrastructure: true } : {}),
|
|
2889
|
+
...(sliceRules.length > 0 ? { sliceRules } : {}),
|
|
2561
2890
|
mayImport,
|
|
2562
2891
|
mustNotImport,
|
|
2563
2892
|
note:
|
|
@@ -2593,7 +2922,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2593
2922
|
content: [
|
|
2594
2923
|
{
|
|
2595
2924
|
type: 'text',
|
|
2596
|
-
text: 'ark_prepare_write requires "source" (string). Optional:
|
|
2925
|
+
text: 'ark_prepare_write requires "source" (string) and "filePath". Optional: description, layer.',
|
|
2597
2926
|
},
|
|
2598
2927
|
],
|
|
2599
2928
|
isError: true,
|
|
@@ -2610,18 +2939,15 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2610
2939
|
placement.layer ||
|
|
2611
2940
|
params?.arguments?.layer ||
|
|
2612
2941
|
inferLayer(placement.filePath, config, args.root);
|
|
2613
|
-
const
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
layer,
|
|
2623
|
-
}),
|
|
2624
|
-
});
|
|
2942
|
+
const context = snippetContext({
|
|
2943
|
+
root: args.root,
|
|
2944
|
+
config,
|
|
2945
|
+
filePath: placement.filePath,
|
|
2946
|
+
layer,
|
|
2947
|
+
arkRulesSnapshot: effectiveArkRulesSnapshot,
|
|
2948
|
+
surface: 'mcp',
|
|
2949
|
+
});
|
|
2950
|
+
const validateOnce = (src) => validateSnippetAnalysis({ gate, ts, source: src, context });
|
|
2625
2951
|
const result = composePrepareWrite({
|
|
2626
2952
|
source,
|
|
2627
2953
|
placement: { ...placement, layer },
|
|
@@ -2673,16 +2999,9 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2673
2999
|
tsconfig: args.tsconfig,
|
|
2674
3000
|
manifest: projectManifest,
|
|
2675
3001
|
});
|
|
2676
|
-
return
|
|
2677
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
2678
|
-
structuredContent: result,
|
|
2679
|
-
isError: !result.valid,
|
|
2680
|
-
};
|
|
3002
|
+
return validityToolResult(result);
|
|
2681
3003
|
} catch (error) {
|
|
2682
|
-
return
|
|
2683
|
-
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
2684
|
-
isError: true,
|
|
2685
|
-
};
|
|
3004
|
+
return toolErrorResult(error);
|
|
2686
3005
|
}
|
|
2687
3006
|
}
|
|
2688
3007
|
|
|
@@ -2690,7 +3009,9 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2690
3009
|
try {
|
|
2691
3010
|
const status = buildProjectStatusManifest({
|
|
2692
3011
|
root: args.root,
|
|
2693
|
-
|
|
3012
|
+
// Pass the server's own resolved contract through (a --config in a subdirectory
|
|
3013
|
+
// must not collapse to <root>/ark.config.json); keep upward discovery otherwise.
|
|
3014
|
+
config: args.configExplicit ? configPath : 'ark.config.json',
|
|
2694
3015
|
expectedRoot: binding?.expectedRoot ?? _params?.arguments?.project?.expectedRoot,
|
|
2695
3016
|
expectedProjectId:
|
|
2696
3017
|
binding?.expectedProjectId ?? _params?.arguments?.project?.expectedProjectId,
|
|
@@ -2721,68 +3042,25 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2721
3042
|
isError: binding?.status === 'mismatch',
|
|
2722
3043
|
};
|
|
2723
3044
|
} catch (error) {
|
|
2724
|
-
return
|
|
2725
|
-
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
2726
|
-
isError: true,
|
|
2727
|
-
};
|
|
3045
|
+
return toolErrorResult(error);
|
|
2728
3046
|
}
|
|
2729
3047
|
}
|
|
2730
3048
|
|
|
2731
3049
|
function runRulesInventoryTool() {
|
|
2732
3050
|
try {
|
|
2733
|
-
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
const layer = layerForFile(args.root, file, config.layers);
|
|
2741
|
-
if (layer) fileLayers[rel] = layer;
|
|
2742
|
-
} catch {
|
|
2743
|
-
/* skip */
|
|
2744
|
-
}
|
|
2745
|
-
}
|
|
2746
|
-
const contracted = [];
|
|
2747
|
-
for (const rule of effectiveArkRulesSnapshot.arkRules?.structure ?? []) {
|
|
2748
|
-
contracted.push(rule.id);
|
|
2749
|
-
}
|
|
2750
|
-
for (const inv of effectiveArkRulesSnapshot.arkRules?.invariants ?? []) {
|
|
2751
|
-
contracted.push(inv.id);
|
|
2752
|
-
}
|
|
2753
|
-
const inventory = buildRulesInventory({
|
|
2754
|
-
fileContents,
|
|
2755
|
-
fileLayers,
|
|
2756
|
-
layerContexts: (config.layers ?? []).map((layer) => ({
|
|
2757
|
-
name: layer.name,
|
|
2758
|
-
intentPrefixes: layer.intentPrefixes ?? [],
|
|
2759
|
-
})),
|
|
2760
|
-
contractedRuleIds: contracted,
|
|
2761
|
-
});
|
|
2762
|
-
const nextPilot =
|
|
2763
|
-
inventory.candidates[0] != null
|
|
2764
|
-
? inventoryToExtractionCard(inventory.candidates[0])
|
|
2765
|
-
: null;
|
|
2766
|
-
const payload = {
|
|
2767
|
-
ok: true,
|
|
2768
|
-
rulesInventory: inventory,
|
|
2769
|
-
rulesMigration: {
|
|
2770
|
-
inventoried: inventory.inventoried,
|
|
2771
|
-
underContract: inventory.underContract,
|
|
2772
|
-
frozen: inventory.frozen,
|
|
2773
|
-
notAScore: true,
|
|
2774
|
-
},
|
|
2775
|
-
nextPilot,
|
|
2776
|
-
};
|
|
3051
|
+
// Same helper and keys as `ark-check --rules-inventory --json` (incl. coverageEvidence).
|
|
3052
|
+
const { payload: inventoryPayload } = buildRulesInventoryPayload(
|
|
3053
|
+
args.root,
|
|
3054
|
+
config,
|
|
3055
|
+
collectGovernedFiles(args.root, config)
|
|
3056
|
+
);
|
|
3057
|
+
const payload = { ok: true, ...inventoryPayload };
|
|
2777
3058
|
return {
|
|
2778
3059
|
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
2779
3060
|
structuredContent: payload,
|
|
2780
3061
|
};
|
|
2781
3062
|
} catch (error) {
|
|
2782
|
-
return
|
|
2783
|
-
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
2784
|
-
isError: true,
|
|
2785
|
-
};
|
|
3063
|
+
return toolErrorResult(error);
|
|
2786
3064
|
}
|
|
2787
3065
|
}
|
|
2788
3066
|
|
|
@@ -2816,10 +3094,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2816
3094
|
isError: false,
|
|
2817
3095
|
};
|
|
2818
3096
|
} catch (error) {
|
|
2819
|
-
return
|
|
2820
|
-
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
2821
|
-
isError: true,
|
|
2822
|
-
};
|
|
3097
|
+
return toolErrorResult(error);
|
|
2823
3098
|
}
|
|
2824
3099
|
}
|
|
2825
3100
|
|
|
@@ -2901,6 +3176,10 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2901
3176
|
reply(id, staleProcessFailureResult(binding));
|
|
2902
3177
|
return;
|
|
2903
3178
|
}
|
|
3179
|
+
if (params?.name !== 'ark_identity' && contractStale()) {
|
|
3180
|
+
reply(id, staleContractFailureResult(binding));
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
2904
3183
|
try {
|
|
2905
3184
|
reply(id, withProjectContext(handler(params, binding), binding));
|
|
2906
3185
|
} catch (error) {
|