arkgate 4.8.22 → 4.8.24
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 +91 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +27 -78
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +1117 -38
- package/bin/ark-mcp-runtime.mjs +412 -138
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +39 -0
- 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/agent-projection-command.mjs +5 -1
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +50 -6
- package/bin/lib/ark-order-doctor.mjs +1 -1
- package/bin/lib/ark-order-facts.mjs +410 -15
- package/bin/lib/ark-order-invariants.mjs +51 -9
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +2 -0
- package/bin/lib/ark-run-doctor.mjs +14 -0
- package/bin/lib/ark-run-facts.mjs +434 -11
- package/bin/lib/ark-run-sensors.mjs +5 -3
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrules-sensors.mjs +50 -249
- package/bin/lib/check-args.mjs +23 -1
- package/bin/lib/ci-and-commands.mjs +16 -6
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/codex-home.mjs +4 -2
- package/bin/lib/config-contract-slices.mjs +701 -0
- package/bin/lib/config-contract.mjs +5 -2
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +45 -6
- package/bin/lib/doctor-advisories.mjs +84 -1
- package/bin/lib/doctor-next-actions.mjs +17 -2
- package/bin/lib/doctor-plan.mjs +7 -9
- package/bin/lib/effective-contract-load.mjs +121 -59
- package/bin/lib/enforcement-honesty.mjs +22 -0
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/first-run-help.mjs +3 -2
- package/bin/lib/flat-parent-importers.mjs +137 -0
- package/bin/lib/flat-parent-pilot.mjs +192 -0
- package/bin/lib/gate-files.mjs +29 -2
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +1 -1
- package/bin/lib/hook-templates.mjs +18 -16
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/html-report-advisories.mjs +2 -0
- package/bin/lib/html-report-depth.mjs +3 -1
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage.mjs +59 -11
- package/bin/lib/mcp-adoption.mjs +9 -1
- package/bin/lib/mcp-hook-payload.mjs +401 -61
- package/bin/lib/package-manager.mjs +59 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +81 -0
- package/bin/lib/physical-cohesion.mjs +16 -18
- package/bin/lib/pilot-loop.mjs +21 -8
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +168 -10
- package/bin/lib/prepare-change.mjs +120 -6
- package/bin/lib/prepare-write.mjs +1 -0
- package/bin/lib/presets.mjs +3 -5
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +315 -55
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +179 -5
- package/bin/lib/scan-files.mjs +32 -0
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/stable-hash.mjs +90 -0
- package/bin/lib/start-install-recovery.mjs +71 -1
- package/bin/lib/start-preview.mjs +30 -0
- package/bin/lib/status-command.mjs +33 -4
- package/bin/lib/status-manifest.mjs +32 -4
- package/bin/lib/team-parliament-io.mjs +79 -75
- package/bin/lib/team-parliament.mjs +6 -1
- package/bin/lib/write-path-capabilities.mjs +4 -2
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-MBXCFLO4.js +16 -0
- package/dist/chunk-RNQHRDCQ.cjs +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/{configTypes-CTELo87-.d.ts → configTypes-BJbaeoj4.d.cts} +53 -4
- package/dist/configTypes-BJbaeoj4.d.ts +246 -0
- package/dist/eslint/index.cjs +17 -4
- package/dist/eslint/index.d.cts +359 -0
- package/dist/eslint/index.d.ts +178 -158
- package/dist/eslint/index.js +10 -4
- package/dist/index.cjs +38 -37
- package/dist/index.d.cts +5475 -0
- package/dist/index.d.ts +3408 -296
- package/dist/index.js +38 -37
- 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-WD2TPSwZ.d.ts → runtime/index.d.cts} +814 -290
- package/dist/runtime/index.d.ts +2678 -108
- package/dist/runtime/index.js +13 -30
- package/dist/{types-yDNEQ603.d.ts → types-DRinG-vT.d.cts} +576 -140
- package/dist/types-DRinG-vT.d.ts +1792 -0
- package/docs/README.md +3 -1
- package/docs/agent-guide.md +92 -15
- package/docs/ai-gates.md +160 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +273 -12
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +122 -13
- package/docs/package-surface.md +31 -8
- package/docs/product-voice.md +1 -1
- package/package.json +59 -18
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +163 -4
- 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 +4 -1
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- 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 +4 -1
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-runtime.md +6 -1
- package/dist/types-fQ9-KrBj.d.ts +0 -183
package/bin/ark-mcp-runtime.mjs
CHANGED
|
@@ -30,10 +30,8 @@ import { composePrepareWrite } from './lib/prepare-write.mjs';
|
|
|
30
30
|
import { placementDescriptionFields } from './lib/layer-description.mjs';
|
|
31
31
|
import { loadArkConfigContract } from './lib/config-contract.mjs';
|
|
32
32
|
import { loadEffectiveArkRulesFromDisk } from './lib/effective-contract-load.mjs';
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
inventoryToExtractionCard,
|
|
36
|
-
} from './lib/rules-inventory.mjs';
|
|
33
|
+
import { loadArkRulesContract } from './lib/arkrules-contract.mjs';
|
|
34
|
+
import { buildRulesInventoryPayload } from './lib/rules-under-contract.mjs';
|
|
37
35
|
import { ARK_ANALYSIS_RESULT_SCHEMA, createAdapterResult } from './lib/adapter-contract.mjs';
|
|
38
36
|
import {
|
|
39
37
|
ARK_PROJECT_IDENTITY_SCHEMA,
|
|
@@ -63,8 +61,18 @@ function arkRulesCatalogForManifest(snapshot) {
|
|
|
63
61
|
sourceFile: r.provenance?.sourceFile,
|
|
64
62
|
coverage: r.coverage,
|
|
65
63
|
}));
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
// ADR 0012 D2: an unreferenced arkrules/*.json is visible drift (advisory).
|
|
65
|
+
const unreferencedFiles = (snapshot.warnings ?? []).map((w) => w.path).filter(Boolean);
|
|
66
|
+
if (structure.length === 0 && invariants.length === 0 && unreferencedFiles.length === 0) {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
arkRulesCatalog: {
|
|
71
|
+
structure,
|
|
72
|
+
invariants,
|
|
73
|
+
...(unreferencedFiles.length > 0 ? { unreferencedFiles } : {}),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
68
76
|
}
|
|
69
77
|
import { loadTypeScript } from './lib/typescript-host.mjs';
|
|
70
78
|
import { validateSnippetAnalysis } from './lib/snippet-analysis.mjs';
|
|
@@ -86,6 +94,8 @@ import {
|
|
|
86
94
|
formatWriteGateDeny,
|
|
87
95
|
unclassifiedIncludedWriteDeny,
|
|
88
96
|
requiredOwnerWriteDeny,
|
|
97
|
+
relativeToHookRoot,
|
|
98
|
+
canonicalPathLoose,
|
|
89
99
|
} from './lib/mcp-hook-payload.mjs';
|
|
90
100
|
import {
|
|
91
101
|
canonicalizeCandidateChanges,
|
|
@@ -108,6 +118,10 @@ import {
|
|
|
108
118
|
formatDesignDeltaBlock,
|
|
109
119
|
} from './lib/design-delta.mjs';
|
|
110
120
|
import { attachPolicyAdrNote } from './lib/adr-path.mjs';
|
|
121
|
+
import {
|
|
122
|
+
candidateArkRulesMatchProject,
|
|
123
|
+
resolvePolicyDeltaArkRules,
|
|
124
|
+
} from './lib/policy-delta-io.mjs';
|
|
111
125
|
|
|
112
126
|
const arkCheckBin = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
|
|
113
127
|
const arkMcpLauncher = fileURLToPath(new URL('./ark-mcp.mjs', import.meta.url));
|
|
@@ -300,8 +314,10 @@ function isResolvedAnalysisInput(relativePath, args, compilerInputs = new Set())
|
|
|
300
314
|
const relative = String(relativePath).replace(/\\/g, '/');
|
|
301
315
|
try {
|
|
302
316
|
const candidate = resolvedInputIdentities(args.root, [relative]);
|
|
317
|
+
// Referenced ArkRules files are contract inputs too: a patch that edits one cannot
|
|
318
|
+
// be preflighted against the catalog loaded from disk.
|
|
303
319
|
const known = resolvedInputIdentities(args.root, [
|
|
304
|
-
args.config, args.manifest, args.tsconfig, ...compilerInputs,
|
|
320
|
+
args.config, args.manifest, args.tsconfig, ...compilerInputs, ...(args.arkRulesInputs ?? []),
|
|
305
321
|
]);
|
|
306
322
|
if ([...candidate].some((identity) => known.has(identity))) return true;
|
|
307
323
|
} catch {
|
|
@@ -345,24 +361,143 @@ function extraMergeTeethClassification(root, config) {
|
|
|
345
361
|
};
|
|
346
362
|
}
|
|
347
363
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Load the Effective ArkRules the same way ark-check does. Errors are kept (never
|
|
366
|
+
* thrown) so each surface decides: write paths fail closed, manifest/inventory degrade.
|
|
367
|
+
*/
|
|
368
|
+
function loadArkRulesSnapshot(root, config) {
|
|
369
|
+
try {
|
|
370
|
+
const loaded = loadEffectiveArkRulesFromDisk(root, config);
|
|
371
|
+
return {
|
|
372
|
+
arkRules: loaded.arkRules ?? null,
|
|
373
|
+
warnings: loaded.warnings ?? [],
|
|
374
|
+
errors: loaded.errors ?? [],
|
|
375
|
+
};
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return {
|
|
378
|
+
arkRules: null,
|
|
379
|
+
warnings: [],
|
|
380
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Single-file context for validateSnippetAnalysis: layer plane plus every extra plane
|
|
387
|
+
* CI enforces on the same file (ArkRun, ArkRules structure, ArkOrder), with the same
|
|
388
|
+
* classification floor. `surface` only tunes guidance text (hook vs MCP).
|
|
389
|
+
*/
|
|
390
|
+
function snippetContext({
|
|
391
|
+
root,
|
|
392
|
+
config,
|
|
393
|
+
filePath,
|
|
394
|
+
layer,
|
|
395
|
+
relFile,
|
|
396
|
+
classification,
|
|
397
|
+
arkRulesSnapshot,
|
|
398
|
+
surface,
|
|
399
|
+
}) {
|
|
400
|
+
const base = { layer, filePath, ...(surface ? { surface } : {}) };
|
|
401
|
+
const arkRulesOn = (arkRulesSnapshot?.arkRules?.structure?.length ?? 0) > 0;
|
|
402
|
+
const contractErrors = arkRulesSnapshot?.errors ?? [];
|
|
403
|
+
if (!config?.arkRun && !config?.arkOrder && !arkRulesOn && contractErrors.length === 0) {
|
|
404
|
+
return base;
|
|
405
|
+
}
|
|
351
406
|
const relative =
|
|
352
407
|
relFile ||
|
|
353
408
|
(typeof filePath === 'string'
|
|
354
409
|
? path.relative(root, path.resolve(root, filePath)).split(path.sep).join('/')
|
|
355
410
|
: undefined);
|
|
411
|
+
const inRoot =
|
|
412
|
+
typeof relative === 'string' &&
|
|
413
|
+
relative !== '' &&
|
|
414
|
+
relative !== '..' &&
|
|
415
|
+
!relative.startsWith('../') &&
|
|
416
|
+
!path.isAbsolute(relative);
|
|
356
417
|
return {
|
|
357
|
-
|
|
358
|
-
filePath,
|
|
418
|
+
...base,
|
|
359
419
|
relFile: relative,
|
|
360
|
-
|
|
420
|
+
inScope: inRoot ? isCandidateSourceInScope(config, relative) : false,
|
|
421
|
+
...(config.arkRun ? { arkRun: config.arkRun } : {}),
|
|
422
|
+
...(config.arkOrder ? { arkOrder: config.arkOrder } : {}),
|
|
423
|
+
...(arkRulesOn ? { arkRules: arkRulesSnapshot.arkRules } : {}),
|
|
424
|
+
...(contractErrors.length > 0 ? { contractErrors } : {}),
|
|
361
425
|
layers: config.layers ?? [],
|
|
362
426
|
classification: classification ?? extraMergeTeethClassification(root, config),
|
|
363
427
|
};
|
|
364
428
|
}
|
|
365
429
|
|
|
430
|
+
function configuredConfigPath(args) {
|
|
431
|
+
return path.isAbsolute(args.config) ? args.config : path.join(args.root, args.config);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Referenced ArkRules files (config.arkRules: layer -> project-relative path), keyed by
|
|
436
|
+
* canonical absolute path. These are law files exactly like ark.config.json.
|
|
437
|
+
*/
|
|
438
|
+
function referencedArkRulesFiles(args, config) {
|
|
439
|
+
const refs = config?.arkRules;
|
|
440
|
+
const out = new Map();
|
|
441
|
+
if (!refs || typeof refs !== 'object') return out;
|
|
442
|
+
for (const [layer, rel] of Object.entries(refs)) {
|
|
443
|
+
if (typeof rel !== 'string' || rel.length === 0) continue;
|
|
444
|
+
out.set(canonicalPathLoose(path.resolve(args.root, rel)), { layer, rel });
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function lawFileLabel(args, absolute) {
|
|
450
|
+
return (relativeToHookRoot(args.root, absolute) ?? path.basename(absolute))
|
|
451
|
+
.split(path.sep)
|
|
452
|
+
.join('/');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Law-file guard: a write that replaces the configured ark.config.json, or an ArkRules
|
|
457
|
+
* file it references, with content the contract loader rejects (or deletes it) would
|
|
458
|
+
* switch enforcement off for every later write. Deny it up front with the loader's
|
|
459
|
+
* reason. `content: null` / `delete: true` is a deletion.
|
|
460
|
+
*/
|
|
461
|
+
function lawFileWriteDeny(args, config, writes) {
|
|
462
|
+
const configPath = canonicalPathLoose(configuredConfigPath(args));
|
|
463
|
+
const arkRulesFiles = referencedArkRulesFiles(args, config);
|
|
464
|
+
for (const write of writes) {
|
|
465
|
+
if (typeof write?.filePath !== 'string') continue;
|
|
466
|
+
const target = canonicalPathLoose(path.resolve(args.root, write.filePath));
|
|
467
|
+
const isConfig = target === configPath;
|
|
468
|
+
const arkRulesRef = isConfig ? undefined : arkRulesFiles.get(target);
|
|
469
|
+
if (!isConfig && !arkRulesRef) continue;
|
|
470
|
+
const deleting = write.delete === true || write.content === null;
|
|
471
|
+
if (!deleting && typeof write.content !== 'string') continue;
|
|
472
|
+
let reason = null;
|
|
473
|
+
if (deleting) {
|
|
474
|
+
reason = 'the file would be deleted';
|
|
475
|
+
} else {
|
|
476
|
+
try {
|
|
477
|
+
const parsed = JSON.parse(write.content);
|
|
478
|
+
if (isConfig) loadArkConfigContract(parsed, configPath);
|
|
479
|
+
else loadArkRulesContract(parsed, arkRulesRef.rel, arkRulesRef.layer);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
reason = error instanceof Error ? error.message : String(error);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (reason === null) continue;
|
|
485
|
+
const file = isConfig ? lawFileLabel(args, configPath) : arkRulesRef.rel;
|
|
486
|
+
return {
|
|
487
|
+
file,
|
|
488
|
+
message: formatWriteGateDeny({
|
|
489
|
+
file,
|
|
490
|
+
reason: `this write would leave ${file} unloadable, which switches the write gate off: ${reason}`,
|
|
491
|
+
ruleId: 'WRITE_GATE_UNAVAILABLE',
|
|
492
|
+
nextAction: isConfig
|
|
493
|
+
? 'Write a complete, valid contract (same schema ark-check --strict-config reads); do not remove or empty ark.config.json.'
|
|
494
|
+
: `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.`,
|
|
495
|
+
}),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
366
501
|
function designDeltaViolations(delta) {
|
|
367
502
|
return (delta?.changes ?? []).map((change) => ({
|
|
368
503
|
ruleId: 'DESIGN_SMELL_REGRESSION',
|
|
@@ -418,10 +553,30 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
418
553
|
// Codex ApplyPatch is only preflighted when Ark can reconstruct every file operation.
|
|
419
554
|
// An incomplete reconstruction must not be mislabeled as atomic or hard enforcement.
|
|
420
555
|
if (!parsedPatch.complete) {
|
|
556
|
+
// Documented residual: a patch Ark cannot reconstruct stays CI-backed. Say so
|
|
557
|
+
// instead of allowing silently.
|
|
558
|
+
output.stderr(
|
|
559
|
+
'Ark architecture gate: this apply_patch could not be fully reconstructed, so it was ' +
|
|
560
|
+
'not preflighted here; CI (--strict-merge) still checks it.\n'
|
|
561
|
+
);
|
|
421
562
|
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
422
563
|
return;
|
|
423
564
|
}
|
|
424
565
|
const patchWrites = parsedPatch.writes;
|
|
566
|
+
const lawDeny = lawFileWriteDeny(
|
|
567
|
+
args,
|
|
568
|
+
config,
|
|
569
|
+
patchWrites.map((change) => ({
|
|
570
|
+
filePath: change.filePath,
|
|
571
|
+
content: change.delete === true ? null : change.content,
|
|
572
|
+
delete: change.delete === true,
|
|
573
|
+
}))
|
|
574
|
+
);
|
|
575
|
+
if (lawDeny) {
|
|
576
|
+
emitHostDeny(output, { antigravityStyle, cursorStyle, grokStyle, ...lawDeny });
|
|
577
|
+
output.status(2);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
425
580
|
const patchChangeSet = classifyChangeSet(patchWrites.map((change) => String(change.path)));
|
|
426
581
|
const patchLawGate = evaluateTeamGate({
|
|
427
582
|
changeSet: patchChangeSet,
|
|
@@ -608,14 +763,17 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
608
763
|
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
609
764
|
return;
|
|
610
765
|
}
|
|
611
|
-
|
|
766
|
+
// Pre-existing (ratcheted) findings ride as warnings; the deny names what blocks.
|
|
767
|
+
const blocking = result.diagnostics.filter((d) => d.severity === 'error');
|
|
768
|
+
const shown = blocking.length > 0 ? blocking : result.diagnostics;
|
|
769
|
+
const first = shown[0];
|
|
612
770
|
const message = formatWriteGateDeny({
|
|
613
771
|
file: `${changes.length} file(s)`,
|
|
614
772
|
reason: first?.message || `this ${toolName} breaks the architecture layers`,
|
|
615
773
|
ruleId: first?.ruleId,
|
|
616
774
|
nextAction: first?.nextAction,
|
|
617
775
|
extraLines: [
|
|
618
|
-
...
|
|
776
|
+
...shown.slice(1).map((d) => `[${d.ruleId}] ${d.message}`),
|
|
619
777
|
...(designDelta && !designDelta.valid
|
|
620
778
|
? formatDesignDeltaBlock(designDelta).split('\n').slice(1)
|
|
621
779
|
: []),
|
|
@@ -639,19 +797,46 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
639
797
|
output.status(2);
|
|
640
798
|
return;
|
|
641
799
|
}
|
|
642
|
-
const
|
|
800
|
+
const rawFilePath = toolInput.file_path;
|
|
643
801
|
if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) {
|
|
644
802
|
// Non-file tools: fail-open. Antigravity still needs an explicit allow decision.
|
|
645
803
|
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
646
804
|
return;
|
|
647
805
|
}
|
|
648
|
-
if (typeof
|
|
806
|
+
if (typeof rawFilePath === 'string' && !SOURCE_FILE.test(rawFilePath)) {
|
|
807
|
+
// Law files are not source, but a write that leaves ark.config.json or a referenced
|
|
808
|
+
// ArkRules file unloadable would silently switch the gate off for every later write.
|
|
809
|
+
const source = proposedSource(toolName, toolInput);
|
|
810
|
+
const lawDeny =
|
|
811
|
+
typeof source === 'string'
|
|
812
|
+
? lawFileWriteDeny(args, config, [{ filePath: rawFilePath, content: source }])
|
|
813
|
+
: null;
|
|
814
|
+
if (lawDeny) {
|
|
815
|
+
emitHostDeny(output, { antigravityStyle, cursorStyle, grokStyle, ...lawDeny });
|
|
816
|
+
output.status(2);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (
|
|
821
|
+
typeof rawFilePath !== 'string' ||
|
|
822
|
+
!SOURCE_FILE.test(rawFilePath) ||
|
|
823
|
+
rawFilePath.endsWith('.d.ts')
|
|
824
|
+
) {
|
|
649
825
|
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
650
826
|
return;
|
|
651
827
|
}
|
|
652
|
-
|
|
828
|
+
// Containment compares the caller's spelling first, then the canonical (realpath)
|
|
829
|
+
// spelling: /tmp vs /private/tmp or a symlinked checkout is the same workspace.
|
|
830
|
+
// Every downstream consumer then sees the path re-spelled under args.root.
|
|
831
|
+
const rootRelative = relativeToHookRoot(args.root, path.resolve(rawFilePath));
|
|
832
|
+
if (rootRelative === null) {
|
|
833
|
+
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const rel = rootRelative;
|
|
837
|
+
const filePath = path.join(args.root, rel);
|
|
653
838
|
const segments = rel.split(path.sep);
|
|
654
|
-
if (segments
|
|
839
|
+
if (segments.includes('node_modules')) {
|
|
655
840
|
emitHostAllow(output, { antigravityStyle, cursorStyle });
|
|
656
841
|
return;
|
|
657
842
|
}
|
|
@@ -708,19 +893,17 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
708
893
|
output.status(2);
|
|
709
894
|
return;
|
|
710
895
|
}
|
|
896
|
+
const context = snippetContext({
|
|
897
|
+
root: args.root,
|
|
898
|
+
config,
|
|
899
|
+
filePath,
|
|
900
|
+
layer,
|
|
901
|
+
relFile: normalizedRel,
|
|
902
|
+
arkRulesSnapshot: args.arkRulesSnapshot,
|
|
903
|
+
surface: 'hook',
|
|
904
|
+
});
|
|
711
905
|
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
|
-
});
|
|
906
|
+
validateSnippetAnalysis({ gate, ts, source: src, context });
|
|
724
907
|
// W1: one validation pass (+ optional autoPatch). Original write still blocked when
|
|
725
908
|
// invalid; hosts must apply autoPatch explicitly (never silent write).
|
|
726
909
|
const result = ts
|
|
@@ -761,12 +944,18 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
761
944
|
// every subsequent edit to that file un-writable while CI passes. Same-file keys ignore
|
|
762
945
|
// line numbers (edits shift them); simpler than full baselineKey (no file/layer fields
|
|
763
946
|
// needed — this file is fixed).
|
|
947
|
+
// Contract-level failures (WRITE_GATE_UNAVAILABLE: a referenced ArkRules file missing
|
|
948
|
+
// or invalid) say nothing about this file's content. The on-disk version reports the
|
|
949
|
+
// same one, so ratcheting it would cancel it and let every edit to an existing file
|
|
950
|
+
// skip the sensors that could not load. They are never pre-existing: always deny.
|
|
951
|
+
const ratchetExempt = (rule) =>
|
|
952
|
+
rule.startsWith('ANALYSIS_') || rule === 'WRITE_GATE_UNAVAILABLE';
|
|
764
953
|
const violationKey = (violation) => `${violation.ruleId}|${violation.target ?? violation.message}`;
|
|
765
954
|
let existingCounts = new Map();
|
|
766
955
|
try {
|
|
767
956
|
const current = fs.readFileSync(filePath, 'utf8');
|
|
768
957
|
for (const violation of validateOnce(current).violations) {
|
|
769
|
-
if (String(violation.ruleId ?? violation.code)
|
|
958
|
+
if (ratchetExempt(String(violation.ruleId ?? violation.code))) continue;
|
|
770
959
|
const key = violationKey(violation);
|
|
771
960
|
existingCounts.set(key, (existingCounts.get(key) ?? 0) + 1);
|
|
772
961
|
}
|
|
@@ -777,7 +966,7 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
|
|
|
777
966
|
const rule = String(violation.ruleId ?? violation.code);
|
|
778
967
|
// Incremental mid-edit parse errors are normal for agents — do not deny solely on them.
|
|
779
968
|
if (rule === 'ANALYSIS_PARSE_INCOMPLETE') return false;
|
|
780
|
-
if (rule
|
|
969
|
+
if (ratchetExempt(rule)) return true;
|
|
781
970
|
const key = violationKey(violation);
|
|
782
971
|
const remaining = existingCounts.get(key) ?? 0;
|
|
783
972
|
if (remaining === 0) return true;
|
|
@@ -961,7 +1150,7 @@ function residentCompilerInputs(ts, args) {
|
|
|
961
1150
|
return [...inputs];
|
|
962
1151
|
}
|
|
963
1152
|
|
|
964
|
-
function residentHookInputs(ts, args) {
|
|
1153
|
+
function residentHookInputs(ts, args, config) {
|
|
965
1154
|
const configPath = path.isAbsolute(args.config)
|
|
966
1155
|
? args.config
|
|
967
1156
|
: path.join(args.root, args.config);
|
|
@@ -973,6 +1162,7 @@ function residentHookInputs(ts, args) {
|
|
|
973
1162
|
return [
|
|
974
1163
|
configPath,
|
|
975
1164
|
...(manifestPath ? [manifestPath] : []),
|
|
1165
|
+
...arkRulesInputPaths(args.root, config),
|
|
976
1166
|
...residentCompilerInputs(ts, args),
|
|
977
1167
|
...[
|
|
978
1168
|
'package.json',
|
|
@@ -985,6 +1175,15 @@ function residentHookInputs(ts, args) {
|
|
|
985
1175
|
];
|
|
986
1176
|
}
|
|
987
1177
|
|
|
1178
|
+
/** Referenced ArkRules files (existing or not): editing/deleting one invalidates caches. */
|
|
1179
|
+
function arkRulesInputPaths(root, config) {
|
|
1180
|
+
const refs = config?.arkRules;
|
|
1181
|
+
if (!refs || typeof refs !== 'object') return [];
|
|
1182
|
+
return Object.values(refs)
|
|
1183
|
+
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
1184
|
+
.map((value) => path.resolve(root, value));
|
|
1185
|
+
}
|
|
1186
|
+
|
|
988
1187
|
function captureResidentHook(payload, gate, config, args, ts, request) {
|
|
989
1188
|
let stdout = '';
|
|
990
1189
|
let stderr = '';
|
|
@@ -1121,7 +1320,7 @@ async function startResidentHookControl({ args, gate, config, ts, loadedTypeScri
|
|
|
1121
1320
|
launcher: arkMcpLauncher,
|
|
1122
1321
|
});
|
|
1123
1322
|
const identityPaths = [
|
|
1124
|
-
...residentHookInputs(ts, args),
|
|
1323
|
+
...residentHookInputs(ts, args, config),
|
|
1125
1324
|
...(loadedTypeScript?.resolvedPath ? [loadedTypeScript.resolvedPath] : []),
|
|
1126
1325
|
];
|
|
1127
1326
|
const identityTokens = [
|
|
@@ -1289,10 +1488,12 @@ function printSessionContext(config, profile, forbiddenGlobals, args, configPath
|
|
|
1289
1488
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
1290
1489
|
}
|
|
1291
1490
|
|
|
1292
|
-
export async function runArkMcp({ hookInput } = {}) {
|
|
1491
|
+
export async function runArkMcp({ hookInput, hookRoot } = {}) {
|
|
1293
1492
|
const processStartedAt = new Date().toISOString();
|
|
1294
1493
|
const runtimeId = randomUUID();
|
|
1295
1494
|
const args = parseArgs(process.argv);
|
|
1495
|
+
// The launcher already resolved the hook's project root (walk-up from the payload).
|
|
1496
|
+
if (args.hook && typeof hookRoot === 'string' && hookRoot) args.root = hookRoot;
|
|
1296
1497
|
const requestedRoot = args.root;
|
|
1297
1498
|
const resolvedRoot = canonicalPathIncludingMissing(requestedRoot);
|
|
1298
1499
|
// MCP identity and file containment use the canonical workspace. Hook payloads,
|
|
@@ -1437,6 +1638,12 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1437
1638
|
},
|
|
1438
1639
|
});
|
|
1439
1640
|
|
|
1641
|
+
// Effective ArkRules load once, before the hook: write paths run the same
|
|
1642
|
+
// structure sensors CI runs and fail closed when a referenced file is broken.
|
|
1643
|
+
const effectiveArkRulesSnapshot = loadArkRulesSnapshot(args.root, config);
|
|
1644
|
+
args.arkRulesSnapshot = effectiveArkRulesSnapshot;
|
|
1645
|
+
args.arkRulesInputs = arkRulesInputPaths(args.root, config);
|
|
1646
|
+
|
|
1440
1647
|
if (args.hook) {
|
|
1441
1648
|
runHook(gate, config, args, ts, hookInput);
|
|
1442
1649
|
return;
|
|
@@ -1447,23 +1654,6 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1447
1654
|
return;
|
|
1448
1655
|
}
|
|
1449
1656
|
|
|
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
1657
|
const residentHookControl = await startResidentHookControl({
|
|
1468
1658
|
args,
|
|
1469
1659
|
gate,
|
|
@@ -1474,6 +1664,13 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1474
1664
|
});
|
|
1475
1665
|
if (residentHookControl) process.once('exit', residentHookControl.cleanup);
|
|
1476
1666
|
|
|
1667
|
+
const contractInputPaths = [
|
|
1668
|
+
configPath,
|
|
1669
|
+
...(manifestPath ? [manifestPath] : []),
|
|
1670
|
+
...arkRulesInputPaths(args.root, config),
|
|
1671
|
+
].filter(Boolean);
|
|
1672
|
+
const startupContractIdentity = residentEnvironmentIdentity(contractInputPaths);
|
|
1673
|
+
|
|
1477
1674
|
const SERVER_INFO = { name: 'arkgate', version: ark.version };
|
|
1478
1675
|
const DEFAULT_PROTOCOL = '2024-11-05';
|
|
1479
1676
|
const projectIdentity = createProjectIdentity({
|
|
@@ -1569,6 +1766,31 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1569
1766
|
overallOk: { type: 'boolean' },
|
|
1570
1767
|
},
|
|
1571
1768
|
};
|
|
1769
|
+
// withProjectContext always spreads processPackage (FX06) into structuredContent;
|
|
1770
|
+
// both oneOf branches are additionalProperties:false, so it must be declared.
|
|
1771
|
+
const processPackageOutputSchema = {
|
|
1772
|
+
type: 'object',
|
|
1773
|
+
additionalProperties: false,
|
|
1774
|
+
required: [
|
|
1775
|
+
'schemaVersion',
|
|
1776
|
+
'notAScore',
|
|
1777
|
+
'processArkgateVersion',
|
|
1778
|
+
'projectInstalledVersion',
|
|
1779
|
+
'processPackageMismatch',
|
|
1780
|
+
'processStale',
|
|
1781
|
+
'nextAction',
|
|
1782
|
+
],
|
|
1783
|
+
properties: {
|
|
1784
|
+
schemaVersion: { const: '1.0' },
|
|
1785
|
+
notAScore: { const: true },
|
|
1786
|
+
processArkgateVersion: { type: ['string', 'null'] },
|
|
1787
|
+
projectInstalledVersion: { type: ['string', 'null'] },
|
|
1788
|
+
processPackageMismatch: { type: 'boolean' },
|
|
1789
|
+
processStale: { type: 'boolean' },
|
|
1790
|
+
nextAction: { type: 'string', minLength: 1 },
|
|
1791
|
+
},
|
|
1792
|
+
};
|
|
1793
|
+
const contractStaleOutputSchema = { type: 'boolean' };
|
|
1572
1794
|
const analysisResultWithProjectSchema = {
|
|
1573
1795
|
type: ARK_ANALYSIS_RESULT_SCHEMA.type,
|
|
1574
1796
|
additionalProperties: ARK_ANALYSIS_RESULT_SCHEMA.additionalProperties,
|
|
@@ -1578,19 +1800,23 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1578
1800
|
'projectIdentity',
|
|
1579
1801
|
'binding',
|
|
1580
1802
|
'authoritative',
|
|
1803
|
+
'processPackage',
|
|
1581
1804
|
],
|
|
1582
1805
|
properties: {
|
|
1583
1806
|
...ARK_ANALYSIS_RESULT_SCHEMA.properties,
|
|
1584
1807
|
projectIdentity: projectIdentityOutputSchema,
|
|
1585
1808
|
binding: PROJECT_BINDING_SCHEMA,
|
|
1586
1809
|
authoritative: { type: 'boolean' },
|
|
1810
|
+
processPackage: processPackageOutputSchema,
|
|
1811
|
+
contractStale: contractStaleOutputSchema,
|
|
1812
|
+
lexicalValid: { type: 'boolean' },
|
|
1587
1813
|
verdict: checkVerdictOutputSchema,
|
|
1588
1814
|
},
|
|
1589
1815
|
};
|
|
1590
1816
|
const projectBindingErrorSchema = {
|
|
1591
1817
|
type: 'object',
|
|
1592
1818
|
additionalProperties: false,
|
|
1593
|
-
required: ['ok', 'error', 'projectIdentity', 'binding', 'authoritative'],
|
|
1819
|
+
required: ['ok', 'error', 'projectIdentity', 'binding', 'authoritative', 'processPackage'],
|
|
1594
1820
|
properties: {
|
|
1595
1821
|
ok: { const: false },
|
|
1596
1822
|
error: {
|
|
@@ -1605,9 +1831,13 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1605
1831
|
projectIdentity: projectIdentityOutputSchema,
|
|
1606
1832
|
binding: PROJECT_BINDING_SCHEMA,
|
|
1607
1833
|
authoritative: { type: 'boolean' },
|
|
1834
|
+
processPackage: processPackageOutputSchema,
|
|
1835
|
+
contractStale: contractStaleOutputSchema,
|
|
1608
1836
|
},
|
|
1609
1837
|
};
|
|
1838
|
+
// MCP (and the TypeScript SDK) require outputSchema.type === 'object' at the top level.
|
|
1610
1839
|
const projectAwareAnalysisResultSchema = {
|
|
1840
|
+
type: 'object',
|
|
1611
1841
|
oneOf: [analysisResultWithProjectSchema, projectBindingErrorSchema],
|
|
1612
1842
|
};
|
|
1613
1843
|
|
|
@@ -1812,15 +2042,28 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1812
2042
|
});
|
|
1813
2043
|
}
|
|
1814
2044
|
|
|
2045
|
+
/**
|
|
2046
|
+
* Contract inputs (config, manifest, referenced ArkRules) are loaded once at startup.
|
|
2047
|
+
* An edit afterwards makes this process's gate, manifest, and contractHash stale:
|
|
2048
|
+
* report it and fail project tools closed (CONTRACT_STALE) instead of answering
|
|
2049
|
+
* authoritatively from the old contract while ark_check reads the new one.
|
|
2050
|
+
*/
|
|
2051
|
+
function contractStale() {
|
|
2052
|
+
return residentEnvironmentIdentity(contractInputPaths) !== startupContractIdentity;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1815
2055
|
function contextFor(binding) {
|
|
1816
2056
|
const processPackage = processPackageHonesty();
|
|
2057
|
+
const stale = contractStale();
|
|
1817
2058
|
return {
|
|
1818
2059
|
projectIdentity,
|
|
1819
2060
|
binding,
|
|
1820
2061
|
// A correctly bound project is still non-authoritative when this long-lived
|
|
1821
|
-
// process loaded a different package version than the project
|
|
1822
|
-
|
|
2062
|
+
// process loaded a different package version (or contract) than the project
|
|
2063
|
+
// now has on disk.
|
|
2064
|
+
authoritative: binding.authoritative && !processPackage.processStale && !stale,
|
|
1823
2065
|
processPackage,
|
|
2066
|
+
...(stale ? { contractStale: true } : {}),
|
|
1824
2067
|
};
|
|
1825
2068
|
}
|
|
1826
2069
|
|
|
@@ -1897,6 +2140,31 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1897
2140
|
);
|
|
1898
2141
|
}
|
|
1899
2142
|
|
|
2143
|
+
function staleContractFailureResult(binding) {
|
|
2144
|
+
return withProjectContext(
|
|
2145
|
+
{
|
|
2146
|
+
content: [
|
|
2147
|
+
{
|
|
2148
|
+
type: 'text',
|
|
2149
|
+
text: JSON.stringify({
|
|
2150
|
+
ok: false,
|
|
2151
|
+
error: {
|
|
2152
|
+
code: 'CONTRACT_STALE',
|
|
2153
|
+
message:
|
|
2154
|
+
`ark.config.json, the project manifest, or a referenced ArkRules file changed ` +
|
|
2155
|
+
`since this MCP process started (contractHash ${projectIdentity.contractHash}). ` +
|
|
2156
|
+
'Restart the MCP server, then re-run ark_identity. Until then use the ' +
|
|
2157
|
+
'project-local CLI (npx arkgate-check), which reads the current contract.',
|
|
2158
|
+
},
|
|
2159
|
+
}),
|
|
2160
|
+
},
|
|
2161
|
+
],
|
|
2162
|
+
isError: true,
|
|
2163
|
+
},
|
|
2164
|
+
binding
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
1900
2168
|
function staleProcessFailureResult(binding) {
|
|
1901
2169
|
const processPackage = processPackageHonesty();
|
|
1902
2170
|
return withProjectContext(
|
|
@@ -1941,12 +2209,16 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
1941
2209
|
{
|
|
1942
2210
|
name: 'validate_code',
|
|
1943
2211
|
description:
|
|
1944
|
-
"
|
|
1945
|
-
'(forbidden infra imports, unknown intents,
|
|
1946
|
-
'
|
|
1947
|
-
'
|
|
1948
|
-
'
|
|
1949
|
-
'
|
|
2212
|
+
"Single-file lexical check of a source snippet against Ark's architecture " +
|
|
2213
|
+
'(forbidden infra imports, unknown intents, layer-reference violations, and the enforced ' +
|
|
2214
|
+
'ArkRun / ArkRules structure / ArkOrder sensors CI runs on that file). One file can never ' +
|
|
2215
|
+
'prove the whole candidate, so the result is always partial: valid:false and isError:true ' +
|
|
2216
|
+
'until complete-candidate preflight. Read lexicalValid and violations for the snippet ' +
|
|
2217
|
+
'verdict; use ark_prepare_change (or arkgate-check) for a green result. For hard ' +
|
|
2218
|
+
'write-path blocking use the `arkgate-mcp --hook` PreToolUse hook, not this tool. ' +
|
|
2219
|
+
'Returns { valid, lexicalValid, completeness, violations, autoPatch? }. autoPatch (when ' +
|
|
2220
|
+
'present) is a mechanical-safe rewrite of the source (import type conversion); hosts may ' +
|
|
2221
|
+
'apply it instead of re-drafting.',
|
|
1950
2222
|
inputSchema: {
|
|
1951
2223
|
type: 'object',
|
|
1952
2224
|
properties: {
|
|
@@ -2009,6 +2281,20 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2009
2281
|
type: 'object',
|
|
2010
2282
|
description: 'Candidate complete config; defaults to the current project contract.',
|
|
2011
2283
|
},
|
|
2284
|
+
baseArkRuleFiles: {
|
|
2285
|
+
type: 'object',
|
|
2286
|
+
description:
|
|
2287
|
+
'Base ArkRules catalog as data: { "<path from baseConfig.arkRules>": <ArkRules file JSON> }. ' +
|
|
2288
|
+
'Required when baseConfig maps arkRules; otherwise the call is refused rather than ' +
|
|
2289
|
+
'classifying ArkRule demotions/deletions as neutral.',
|
|
2290
|
+
},
|
|
2291
|
+
candidateArkRuleFiles: {
|
|
2292
|
+
type: 'object',
|
|
2293
|
+
description:
|
|
2294
|
+
'Candidate ArkRules catalog as data, same shape. Defaults to the files on disk when ' +
|
|
2295
|
+
'candidateConfig is omitted or maps the same arkRules files as the project contract ' +
|
|
2296
|
+
'(the project ark.config.json passed verbatim counts).',
|
|
2297
|
+
},
|
|
2012
2298
|
acknowledgement: {
|
|
2013
2299
|
type: 'object',
|
|
2014
2300
|
description:
|
|
@@ -2052,12 +2338,13 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2052
2338
|
'What you are building. Does not invent a path — pass filePath. Without filePath the tool fail-closes.',
|
|
2053
2339
|
},
|
|
2054
2340
|
},
|
|
2341
|
+
required: ['filePath'],
|
|
2055
2342
|
},
|
|
2056
2343
|
},
|
|
2057
2344
|
{
|
|
2058
2345
|
name: 'ark_prepare_write',
|
|
2059
2346
|
description:
|
|
2060
|
-
'Prepare a write against the architecture contract: place (filePath
|
|
2347
|
+
'Prepare a write against the architecture contract: place (filePath required; fail-closed without it — never invents a path) + ' +
|
|
2061
2348
|
'constrain (layer, mayImport, mustNotImport, forbiddenGlobals) + validate source + optional ' +
|
|
2062
2349
|
'mechanical-safe autoPatch + judgmentBrief when judgment is needed + contentHash for host commit. ' +
|
|
2063
2350
|
'Also returns the versioned new/worsened designDelta for the proposed full file. ' +
|
|
@@ -2065,7 +2352,8 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2065
2352
|
'When the matched layer has layers[].description, the JSON includes description; the field is omitted when absent. ' +
|
|
2066
2353
|
'When the matched layer has layers[].trustBoundary (public|auth|admin|internal), the JSON includes trustBoundary; omitted when absent. ' +
|
|
2067
2354
|
'When the matched layer has layers[].owners, the JSON includes owners; omitted when absent. ' +
|
|
2068
|
-
'
|
|
2355
|
+
'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. ' +
|
|
2356
|
+
'Returns { filePath, layer, description?, trustBoundary?, owners?, valid, lexicalValid, violations?, autoPatch?, judgmentBrief?, contentHash, ... }.',
|
|
2069
2357
|
inputSchema: {
|
|
2070
2358
|
type: 'object',
|
|
2071
2359
|
properties: {
|
|
@@ -2076,14 +2364,15 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2076
2364
|
},
|
|
2077
2365
|
description: {
|
|
2078
2366
|
type: 'string',
|
|
2079
|
-
description:
|
|
2367
|
+
description:
|
|
2368
|
+
'What you are building (hint only). Does not invent a path — pass filePath. Without filePath the tool fail-closes.',
|
|
2080
2369
|
},
|
|
2081
2370
|
layer: {
|
|
2082
2371
|
type: 'string',
|
|
2083
2372
|
description: 'Optional explicit layer override (otherwise inferred from filePath).',
|
|
2084
2373
|
},
|
|
2085
2374
|
},
|
|
2086
|
-
required: ['source'],
|
|
2375
|
+
required: ['source', 'filePath'],
|
|
2087
2376
|
},
|
|
2088
2377
|
},
|
|
2089
2378
|
{
|
|
@@ -2159,6 +2448,8 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2159
2448
|
projectIdentity: projectIdentityOutputSchema,
|
|
2160
2449
|
binding: PROJECT_BINDING_SCHEMA,
|
|
2161
2450
|
authoritative: { type: 'boolean' },
|
|
2451
|
+
processPackage: processPackageOutputSchema,
|
|
2452
|
+
contractStale: contractStaleOutputSchema,
|
|
2162
2453
|
},
|
|
2163
2454
|
},
|
|
2164
2455
|
},
|
|
@@ -2304,18 +2595,15 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2304
2595
|
}
|
|
2305
2596
|
const filePath = params.arguments.filePath;
|
|
2306
2597
|
const layer = params.arguments.layer ?? inferLayer(filePath, config, args.root);
|
|
2307
|
-
const
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
layer,
|
|
2317
|
-
}),
|
|
2318
|
-
});
|
|
2598
|
+
const context = snippetContext({
|
|
2599
|
+
root: args.root,
|
|
2600
|
+
config,
|
|
2601
|
+
filePath,
|
|
2602
|
+
layer,
|
|
2603
|
+
arkRulesSnapshot: effectiveArkRulesSnapshot,
|
|
2604
|
+
surface: 'mcp',
|
|
2605
|
+
});
|
|
2606
|
+
const validateOnce = (src) => validateSnippetAnalysis({ gate, ts, source: src, context });
|
|
2319
2607
|
// W1: attempt mechanical-safe single-file autoPatch (import type), re-validate or discard.
|
|
2320
2608
|
const result = validateWithAutoPatch({
|
|
2321
2609
|
source,
|
|
@@ -2331,6 +2619,12 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2331
2619
|
completenessReasons: result.completenessReasons,
|
|
2332
2620
|
violations: result.violations,
|
|
2333
2621
|
});
|
|
2622
|
+
// lexicalValid is the one-file verdict (valid stays false: single-file evidence
|
|
2623
|
+
// is partial). Same field ark_prepare_write already exposes.
|
|
2624
|
+
const lexicalValid =
|
|
2625
|
+
typeof result.lexicalValid === 'boolean'
|
|
2626
|
+
? result.lexicalValid
|
|
2627
|
+
: (result.violations ?? []).length === 0 && result.completeness !== 'unavailable';
|
|
2334
2628
|
return {
|
|
2335
2629
|
content: [
|
|
2336
2630
|
{
|
|
@@ -2339,6 +2633,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2339
2633
|
{
|
|
2340
2634
|
...adapterResult,
|
|
2341
2635
|
valid: adapterResult.valid,
|
|
2636
|
+
lexicalValid,
|
|
2342
2637
|
violations: result.violations,
|
|
2343
2638
|
...(result.autoPatch ? { autoPatch: result.autoPatch } : {}),
|
|
2344
2639
|
layer,
|
|
@@ -2348,7 +2643,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2348
2643
|
),
|
|
2349
2644
|
},
|
|
2350
2645
|
],
|
|
2351
|
-
structuredContent: adapterResult,
|
|
2646
|
+
structuredContent: { ...adapterResult, lexicalValid },
|
|
2352
2647
|
isError: !adapterResult.valid,
|
|
2353
2648
|
};
|
|
2354
2649
|
}
|
|
@@ -2472,11 +2767,24 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2472
2767
|
}
|
|
2473
2768
|
try {
|
|
2474
2769
|
const acknowledgement = params?.arguments?.acknowledgement;
|
|
2770
|
+
const suppliedCandidate = params?.arguments?.candidateConfig;
|
|
2771
|
+
const candidateConfig = suppliedCandidate ?? config;
|
|
2772
|
+
// ArkRules are part of the policy: classify their transition too, or refuse.
|
|
2773
|
+
const { baseArkRules, candidateArkRules } = resolvePolicyDeltaArkRules({
|
|
2774
|
+
root: args.root,
|
|
2775
|
+
baseConfig,
|
|
2776
|
+
candidateConfig,
|
|
2777
|
+
candidateIsProjectConfig: candidateArkRulesMatchProject(suppliedCandidate, config),
|
|
2778
|
+
baseArkRuleFiles: params?.arguments?.baseArkRuleFiles,
|
|
2779
|
+
candidateArkRuleFiles: params?.arguments?.candidateArkRuleFiles,
|
|
2780
|
+
});
|
|
2475
2781
|
const result = attachPolicyAdrNote(
|
|
2476
2782
|
ark.analyzePolicyDelta({
|
|
2477
2783
|
baseConfig,
|
|
2478
|
-
candidateConfig
|
|
2784
|
+
candidateConfig,
|
|
2479
2785
|
acknowledgement,
|
|
2786
|
+
baseArkRules,
|
|
2787
|
+
candidateArkRules,
|
|
2480
2788
|
}),
|
|
2481
2789
|
{ root: args.root, acknowledgement, failClosed: true }
|
|
2482
2790
|
);
|
|
@@ -2593,7 +2901,7 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2593
2901
|
content: [
|
|
2594
2902
|
{
|
|
2595
2903
|
type: 'text',
|
|
2596
|
-
text: 'ark_prepare_write requires "source" (string). Optional:
|
|
2904
|
+
text: 'ark_prepare_write requires "source" (string) and "filePath". Optional: description, layer.',
|
|
2597
2905
|
},
|
|
2598
2906
|
],
|
|
2599
2907
|
isError: true,
|
|
@@ -2610,18 +2918,15 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2610
2918
|
placement.layer ||
|
|
2611
2919
|
params?.arguments?.layer ||
|
|
2612
2920
|
inferLayer(placement.filePath, config, args.root);
|
|
2613
|
-
const
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
layer,
|
|
2623
|
-
}),
|
|
2624
|
-
});
|
|
2921
|
+
const context = snippetContext({
|
|
2922
|
+
root: args.root,
|
|
2923
|
+
config,
|
|
2924
|
+
filePath: placement.filePath,
|
|
2925
|
+
layer,
|
|
2926
|
+
arkRulesSnapshot: effectiveArkRulesSnapshot,
|
|
2927
|
+
surface: 'mcp',
|
|
2928
|
+
});
|
|
2929
|
+
const validateOnce = (src) => validateSnippetAnalysis({ gate, ts, source: src, context });
|
|
2625
2930
|
const result = composePrepareWrite({
|
|
2626
2931
|
source,
|
|
2627
2932
|
placement: { ...placement, layer },
|
|
@@ -2690,7 +2995,9 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2690
2995
|
try {
|
|
2691
2996
|
const status = buildProjectStatusManifest({
|
|
2692
2997
|
root: args.root,
|
|
2693
|
-
|
|
2998
|
+
// Pass the server's own resolved contract through (a --config in a subdirectory
|
|
2999
|
+
// must not collapse to <root>/ark.config.json); keep upward discovery otherwise.
|
|
3000
|
+
config: args.configExplicit ? configPath : 'ark.config.json',
|
|
2694
3001
|
expectedRoot: binding?.expectedRoot ?? _params?.arguments?.project?.expectedRoot,
|
|
2695
3002
|
expectedProjectId:
|
|
2696
3003
|
binding?.expectedProjectId ?? _params?.arguments?.project?.expectedProjectId,
|
|
@@ -2730,50 +3037,13 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2730
3037
|
|
|
2731
3038
|
function runRulesInventoryTool() {
|
|
2732
3039
|
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
|
-
};
|
|
3040
|
+
// Same helper and keys as `ark-check --rules-inventory --json` (incl. coverageEvidence).
|
|
3041
|
+
const { payload: inventoryPayload } = buildRulesInventoryPayload(
|
|
3042
|
+
args.root,
|
|
3043
|
+
config,
|
|
3044
|
+
collectGovernedFiles(args.root, config)
|
|
3045
|
+
);
|
|
3046
|
+
const payload = { ok: true, ...inventoryPayload };
|
|
2777
3047
|
return {
|
|
2778
3048
|
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
2779
3049
|
structuredContent: payload,
|
|
@@ -2901,6 +3171,10 @@ export async function runArkMcp({ hookInput } = {}) {
|
|
|
2901
3171
|
reply(id, staleProcessFailureResult(binding));
|
|
2902
3172
|
return;
|
|
2903
3173
|
}
|
|
3174
|
+
if (params?.name !== 'ark_identity' && contractStale()) {
|
|
3175
|
+
reply(id, staleContractFailureResult(binding));
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
2904
3178
|
try {
|
|
2905
3179
|
reply(id, withProjectContext(handler(params, binding), binding));
|
|
2906
3180
|
} catch (error) {
|