arkgate 2.12.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/README.md +55 -31
- package/bin/ark-check.mjs +95 -36
- package/bin/ark-mcp.mjs +11 -5
- package/bin/ark-shared.mjs +88 -56
- package/bin/ark.mjs +45 -10
- package/bin/lib/agent-gates.mjs +12 -0
- package/bin/lib/architecture-scan.mjs +8 -0
- package/bin/lib/ci-and-commands.mjs +9 -3
- package/bin/lib/codex-home.mjs +7 -0
- package/bin/lib/config-contract.mjs +331 -0
- package/bin/lib/doctor-plan.mjs +43 -16
- package/bin/lib/enforcement-profiles.mjs +97 -0
- package/bin/lib/host-support-matrix.mjs +77 -0
- package/bin/lib/install-migrate.mjs +45 -14
- package/bin/lib/mcp-adoption.mjs +35 -3
- package/bin/lib/open-html.mjs +75 -0
- package/bin/lib/presets.mjs +3 -2
- package/bin/lib/safety-diagnostics.mjs +31 -11
- package/bin/lib/skill-install.mjs +64 -0
- package/bin/lib/ts-resolve.mjs +2 -1
- package/bin/lib/weakest-link.mjs +417 -0
- package/bin/lib/write-path-capabilities.mjs +182 -0
- package/bin/lib/write-path-detect.mjs +62 -99
- package/dist/configContract-iBLxx5Tz.d.cts +53 -0
- package/dist/configContract-iBLxx5Tz.d.ts +53 -0
- package/dist/eslint/index.cjs +375 -13
- package/dist/eslint/index.cjs.map +1 -1
- package/dist/eslint/index.d.cts +30 -20
- package/dist/eslint/index.d.ts +30 -20
- package/dist/eslint/index.js +375 -13
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.cjs +723 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -5
- package/dist/index.d.ts +95 -5
- package/dist/index.js +716 -61
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +150 -42
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +2 -1
- package/dist/nestjs/index.d.ts +2 -1
- package/dist/nestjs/index.js +150 -42
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +723 -61
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +3 -2
- package/dist/runtime/index.d.ts +3 -2
- package/dist/runtime/index.js +716 -61
- package/dist/runtime/index.js.map +1 -1
- package/dist/{types-BZ17b9i5.d.cts → types-BxBwnBpC.d.cts} +9 -36
- package/dist/{types-BZ17b9i5.d.ts → types-Wcs_l1_J.d.ts} +9 -36
- package/docs/agent-guide.md +32 -20
- package/docs/ai-gates.md +53 -18
- package/docs/configuration.md +97 -0
- package/docs/enthusiast/README.md +3 -3
- package/docs/enthusiast/how-to-agent-gates.md +7 -3
- package/docs/migrate-from-ark-runtime-kernel.md +3 -0
- package/docs/package-surface.md +14 -9
- package/docs/production-hardening.md +15 -2
- package/docs/threat-model.md +65 -0
- package/docs/typescript-support.md +3 -3
- package/package.json +15 -2
- package/schemas/ark.config.schema.json +750 -0
- package/server.json +2 -2
- package/templates/hooks/pre-commit-ark +37 -0
- package/templates/skills/ark-coverage.md +2 -2
- package/templates/skills/ark-runtime.md +8 -5
- package/templates/skills/ark-upgrade.md +36 -16
- package/tests/fixtures/ts-consumer/ark.config.json +2 -0
package/bin/ark-shared.mjs
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_ARK_CONFIG_RULES,
|
|
6
|
+
withArkConfigMetadata,
|
|
7
|
+
} from './lib/config-contract.mjs';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
* Default
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* CLIs run standalone with only `typescript` present, no build step.
|
|
10
|
+
* Default intent-prefix map shared by both CLIs and the ark-mcp write-path gate. The rule
|
|
11
|
+
* matrix comes from the generated Domain config contract above. Prefixes mirror the runtime
|
|
12
|
+
* profile but stay in this standalone CLI module because the CLIs run without a build step.
|
|
10
13
|
*/
|
|
11
14
|
export const DEFAULT_INTENT_PREFIXES = [
|
|
12
15
|
{ layer: 'DomainModel', prefixes: ['Domain.'] },
|
|
@@ -41,35 +44,7 @@ export const DEFAULT_LAYER_DIRECTORIES = {
|
|
|
41
44
|
Kernel: ['kernel'],
|
|
42
45
|
};
|
|
43
46
|
|
|
44
|
-
const
|
|
45
|
-
{ from: 'PresentationAdapters', to: 'ApplicationOrchestration' },
|
|
46
|
-
{ from: 'ApplicationOrchestration', to: 'DomainModel' },
|
|
47
|
-
{ from: 'WorkflowSagaEngine', to: 'ApplicationOrchestration' },
|
|
48
|
-
{ from: 'WorkflowSagaEngine', to: 'DomainModel' },
|
|
49
|
-
{ from: 'BackgroundJobsScheduling', to: 'ApplicationOrchestration' },
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
function flowKey(from, to) {
|
|
53
|
-
return `${from}->${to}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function createStrictDenyRules(layers, allowedFlows) {
|
|
57
|
-
const allowed = new Set(allowedFlows.map((flow) => flowKey(flow.from, flow.to)));
|
|
58
|
-
const rules = [];
|
|
59
|
-
for (const from of layers) {
|
|
60
|
-
for (const to of layers) {
|
|
61
|
-
if (from.layer === to.layer) continue;
|
|
62
|
-
if (allowed.has(flowKey(from.layer, to.layer))) continue;
|
|
63
|
-
rules.push({ from: from.layer, to: to.layer, allowed: false });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return rules;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export const DEFAULT_RULES = createStrictDenyRules(
|
|
70
|
-
DEFAULT_INTENT_PREFIXES,
|
|
71
|
-
DEFAULT_ALLOWED_FLOWS
|
|
72
|
-
);
|
|
47
|
+
export const DEFAULT_RULES = DEFAULT_ARK_CONFIG_RULES;
|
|
73
48
|
|
|
74
49
|
/**
|
|
75
50
|
* Default ambient globals forbidden in the domain layer: a pure domain does no I/O and is
|
|
@@ -82,7 +57,7 @@ export function createElevenLayerConfig(options = {}) {
|
|
|
82
57
|
const rootDir = options.rootDir ?? 'src';
|
|
83
58
|
const optional = options.optionalLayers ?? true;
|
|
84
59
|
const prefix = rootDir === '.' ? '' : `${rootDir}/`;
|
|
85
|
-
const config = {
|
|
60
|
+
const config = withArkConfigMetadata({
|
|
86
61
|
include: options.include ?? [rootDir],
|
|
87
62
|
layers: DEFAULT_INTENT_PREFIXES.map((entry) => ({
|
|
88
63
|
name: entry.layer,
|
|
@@ -96,7 +71,7 @@ export function createElevenLayerConfig(options = {}) {
|
|
|
96
71
|
: {}),
|
|
97
72
|
})),
|
|
98
73
|
rules: DEFAULT_RULES,
|
|
99
|
-
};
|
|
74
|
+
});
|
|
100
75
|
// When a project root is known, overlay Nest/Next/express filename conventions so a
|
|
101
76
|
// flat framework starter is governed on day one (not "0% governed / false green").
|
|
102
77
|
if (options.root) return applyFrameworkLayoutOverlays(config, options.root);
|
|
@@ -134,7 +109,7 @@ export function applyFrameworkLayoutOverlays(config, root) {
|
|
|
134
109
|
try {
|
|
135
110
|
signals = collectRepoShapeSignals(root);
|
|
136
111
|
} catch {
|
|
137
|
-
return config;
|
|
112
|
+
return withArkConfigMetadata(config);
|
|
138
113
|
}
|
|
139
114
|
|
|
140
115
|
const next = {
|
|
@@ -344,7 +319,7 @@ export function applyFrameworkLayoutOverlays(config, root) {
|
|
|
344
319
|
: 'library';
|
|
345
320
|
}
|
|
346
321
|
|
|
347
|
-
return next;
|
|
322
|
+
return withArkConfigMetadata(next);
|
|
348
323
|
}
|
|
349
324
|
|
|
350
325
|
/**
|
|
@@ -398,40 +373,97 @@ export function resolveOperatingMode({
|
|
|
398
373
|
return 'suggest';
|
|
399
374
|
}
|
|
400
375
|
|
|
376
|
+
function singleFileTypeChecker(ts, sourceFile) {
|
|
377
|
+
const options = {
|
|
378
|
+
noLib: true,
|
|
379
|
+
noResolve: true,
|
|
380
|
+
target: ts.ScriptTarget.Latest,
|
|
381
|
+
};
|
|
382
|
+
const host = ts.createCompilerHost(options, true);
|
|
383
|
+
host.getSourceFile = (fileName) =>
|
|
384
|
+
fileName === sourceFile.fileName ? sourceFile : undefined;
|
|
385
|
+
host.fileExists = (fileName) => fileName === sourceFile.fileName;
|
|
386
|
+
host.readFile = (fileName) =>
|
|
387
|
+
fileName === sourceFile.fileName ? sourceFile.text : undefined;
|
|
388
|
+
return ts.createProgram([sourceFile.fileName], options, host).getTypeChecker();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function propertyAccessPath(ts, node) {
|
|
392
|
+
const segments = [];
|
|
393
|
+
let current = node;
|
|
394
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
395
|
+
segments.unshift(current.name.text);
|
|
396
|
+
current = current.expression;
|
|
397
|
+
}
|
|
398
|
+
if (!ts.isIdentifier(current)) return undefined;
|
|
399
|
+
segments.unshift(current.text);
|
|
400
|
+
return { root: current, segments };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isRuntimeIdentifierReference(ts, node) {
|
|
404
|
+
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) return false;
|
|
405
|
+
return (
|
|
406
|
+
(ts.isExpressionNode(node) && !ts.isInTypeQuery(node)) ||
|
|
407
|
+
(ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node)
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function hasLocalDeclaration(ts, checker, sourceFile, node) {
|
|
412
|
+
const shorthand =
|
|
413
|
+
ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node;
|
|
414
|
+
const symbol = shorthand
|
|
415
|
+
? checker.getShorthandAssignmentValueSymbol(node.parent)
|
|
416
|
+
: checker.getSymbolAtLocation(node);
|
|
417
|
+
return Boolean(
|
|
418
|
+
symbol?.declarations?.some((declaration) => declaration.getSourceFile() === sourceFile)
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
401
422
|
/**
|
|
402
423
|
* Find uses of forbidden ambient globals in a TypeScript source file.
|
|
403
424
|
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* - a bare entry ("console", "fetch") flags property accesses on it (`console.log`),
|
|
409
|
-
* direct calls (`fetch(...)`), and constructions (`new WebSocket(...)`)
|
|
410
|
-
* Bare identifier mentions in other positions (types, shadowed locals, import names) are
|
|
411
|
-
* NOT flagged, trading a little recall for near-zero false positives without a type checker.
|
|
425
|
+
* A no-lib, no-resolution TypeScript program binds declarations in this file only. An
|
|
426
|
+
* identifier with a symbol is therefore local (parameter, variable, import, etc.); an
|
|
427
|
+
* unbound runtime identifier is ambient. Dotted entries use AST property chains and
|
|
428
|
+
* explicit `globalThis` access is normalized to the configured global name.
|
|
412
429
|
*
|
|
430
|
+
* Kept in sync with `analyzeForbiddenGlobals` in
|
|
431
|
+
* src/kernel/ai-gate/AICodeGate.ts — the standalone CLIs must not import from dist.
|
|
413
432
|
* Returns [{ name, node }] where `name` is the matched forbidden entry.
|
|
414
433
|
*/
|
|
415
434
|
export function collectForbiddenGlobalUses(ts, sourceFile, forbidden) {
|
|
416
435
|
const entries = new Set(forbidden ?? []);
|
|
417
436
|
if (entries.size === 0) return [];
|
|
437
|
+
const checker = singleFileTypeChecker(ts, sourceFile);
|
|
418
438
|
const uses = [];
|
|
419
439
|
|
|
420
440
|
const visit = (node) => {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
441
|
+
const nestedPropertyAccess =
|
|
442
|
+
ts.isPropertyAccessExpression(node) &&
|
|
443
|
+
ts.isPropertyAccessExpression(node.parent) &&
|
|
444
|
+
node.parent.expression === node;
|
|
445
|
+
if (ts.isPropertyAccessExpression(node) && !nestedPropertyAccess) {
|
|
446
|
+
const path = propertyAccessPath(ts, node);
|
|
447
|
+
if (path && !hasLocalDeclaration(ts, checker, sourceFile, path.root)) {
|
|
448
|
+
const explicitGlobalThis = path.segments[0] === 'globalThis';
|
|
449
|
+
const normalized = explicitGlobalThis ? path.segments.slice(1) : path.segments;
|
|
450
|
+
let match;
|
|
451
|
+
for (let length = normalized.length; length >= (explicitGlobalThis ? 1 : 2); length -= 1) {
|
|
452
|
+
const candidate = normalized.slice(0, length).join('.');
|
|
453
|
+
if (entries.has(candidate)) {
|
|
454
|
+
match = candidate;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (match) uses.push({ name: match, node });
|
|
427
459
|
}
|
|
428
460
|
} else if (
|
|
429
|
-
|
|
430
|
-
node.
|
|
431
|
-
ts
|
|
432
|
-
|
|
461
|
+
ts.isIdentifier(node) &&
|
|
462
|
+
entries.has(node.text) &&
|
|
463
|
+
isRuntimeIdentifierReference(ts, node) &&
|
|
464
|
+
!hasLocalDeclaration(ts, checker, sourceFile, node)
|
|
433
465
|
) {
|
|
434
|
-
uses.push({ name: node.
|
|
466
|
+
uses.push({ name: node.text, node });
|
|
435
467
|
}
|
|
436
468
|
ts.forEachChild(node, visit);
|
|
437
469
|
};
|
package/bin/ark.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
resolveOperatingMode,
|
|
20
20
|
} from './ark-shared.mjs';
|
|
21
21
|
import { pinArkgateDevDependency, FALSE_GREEN_GAP_ID } from './lib/field-install.mjs';
|
|
22
|
+
import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
|
|
22
23
|
|
|
23
24
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
24
25
|
const arkCheck = path.join(here, 'ark-check.mjs');
|
|
@@ -57,6 +58,7 @@ function parseArgs(argv) {
|
|
|
57
58
|
force: false,
|
|
58
59
|
strict: true,
|
|
59
60
|
install: true,
|
|
61
|
+
requireWriteHook: undefined,
|
|
60
62
|
help: false,
|
|
61
63
|
version: false,
|
|
62
64
|
};
|
|
@@ -81,6 +83,9 @@ function parseArgs(argv) {
|
|
|
81
83
|
else if (arg === '--preset') args.preset = requireValue(arg, i++);
|
|
82
84
|
else if (arg === '--archetype') args.archetype = requireValue(arg, i++);
|
|
83
85
|
else if (arg === '--tools') args.tools = requireValue(arg, i++);
|
|
86
|
+
else if (arg === '--require-write-hook') {
|
|
87
|
+
args.requireWriteHook = requireValue(arg, i++).trim().toLowerCase();
|
|
88
|
+
}
|
|
84
89
|
else if (arg === '--help' || arg === '-h' || arg === 'help') args.help = true;
|
|
85
90
|
else if (arg === '--version' || arg === '-V') args.version = true;
|
|
86
91
|
else if (!arg.startsWith('-') && args.command === undefined) args.command = arg;
|
|
@@ -92,9 +97,9 @@ function parseArgs(argv) {
|
|
|
92
97
|
|
|
93
98
|
function usage() {
|
|
94
99
|
return `Usage:
|
|
95
|
-
ark start [--root <project>] [--yes]
|
|
100
|
+
ark start [--root <project>] [--tools <list>] [--require-write-hook <host>] [--yes]
|
|
96
101
|
ark init [--root <project>] [--preset hexagonal|layered|feature-sliced|monorepo|ui-surface|vertical-slice|ddd-bounded-contexts|clean-architecture|onion-architecture]
|
|
97
|
-
[--archetype <playbook-id>] [--tools <list>] [--yes] [--force] [--no-strict]
|
|
102
|
+
[--archetype <playbook-id>] [--tools <list>] [--require-write-hook <host>] [--yes] [--force] [--no-strict]
|
|
98
103
|
ark upgrade [--root <project>] [--no-install] [--no-strict]
|
|
99
104
|
|
|
100
105
|
Commands:
|
|
@@ -119,6 +124,9 @@ Options:
|
|
|
119
124
|
vertical-slice-product, ddd-bounded-contexts.
|
|
120
125
|
--tools Comma-separated agents to gate (claude,cursor,codex,grok,windsurf,cline,copilot,kiro,roo,continue,gemini).
|
|
121
126
|
Omit to auto-detect from each tool's config dir, falling back to claude+cursor+codex+grok.
|
|
127
|
+
--require-write-hook <host>
|
|
128
|
+
Require and verify a hard local write hook for Claude or Grok. Cursor/Codex are
|
|
129
|
+
advisory-write plus hard CI merge only; impossible requests fail before any write.
|
|
122
130
|
|
|
123
131
|
Interactive mode (TTY, no --yes): asks what application shape you are building and maps it to a preset.
|
|
124
132
|
Non-interactive (no TTY): uses the same defaults as --yes — never calls readline on a null interface.
|
|
@@ -203,7 +211,7 @@ async function upgrade(args) {
|
|
|
203
211
|
}
|
|
204
212
|
console.log('\n4/4 Verifying architecture…');
|
|
205
213
|
return runArkCheck(
|
|
206
|
-
['--root', root, '--config', 'ark.config.json', '--strict-
|
|
214
|
+
['--root', root, '--config', 'ark.config.json', '--strict-merge'],
|
|
207
215
|
{ cwd: root }
|
|
208
216
|
);
|
|
209
217
|
}
|
|
@@ -365,6 +373,9 @@ async function init(args) {
|
|
|
365
373
|
if (installGates) {
|
|
366
374
|
const gateArgs = ['--root', root, '--install-agent-gates'];
|
|
367
375
|
if (args.tools) gateArgs.push('--tools', args.tools);
|
|
376
|
+
if (args.requireWriteHook) {
|
|
377
|
+
gateArgs.push('--require-write-hook', args.requireWriteHook);
|
|
378
|
+
}
|
|
368
379
|
if (args.force) gateArgs.push('--force');
|
|
369
380
|
const status = runArkCheck(gateArgs, { cwd: root });
|
|
370
381
|
if (status !== 0) return status;
|
|
@@ -374,14 +385,15 @@ async function init(args) {
|
|
|
374
385
|
args.strict &&
|
|
375
386
|
(nonInteractive || (await askYesNo(rl, 'Run strict architecture check now?', true)));
|
|
376
387
|
if (runStrict) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
388
|
+
const strictArgs = ['--root', root, '--config', 'ark.config.json', '--strict-merge'];
|
|
389
|
+
if (args.requireWriteHook) {
|
|
390
|
+
strictArgs.push('--require-write-hook', args.requireWriteHook);
|
|
391
|
+
}
|
|
392
|
+
return runArkCheck(strictArgs, { cwd: root });
|
|
381
393
|
}
|
|
382
394
|
|
|
383
395
|
console.log(
|
|
384
|
-
`Ark init complete. Run \`${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-
|
|
396
|
+
`Ark init complete. Run \`${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-merge')}\` before merging.`
|
|
385
397
|
);
|
|
386
398
|
if (archetype) {
|
|
387
399
|
console.log(`Shape: ${archetype}. Plan: ${arkCommand(root, 'ark-check', '--recommend')}`);
|
|
@@ -526,8 +538,12 @@ async function start(args) {
|
|
|
526
538
|
{
|
|
527
539
|
const gateArgs = ['--root', root, '--install-agent-gates'];
|
|
528
540
|
if (args.tools) gateArgs.push('--tools', args.tools);
|
|
541
|
+
if (args.requireWriteHook) {
|
|
542
|
+
gateArgs.push('--require-write-hook', args.requireWriteHook);
|
|
543
|
+
}
|
|
529
544
|
if (args.force) gateArgs.push('--force');
|
|
530
|
-
runArkCheck(gateArgs, { cwd: root });
|
|
545
|
+
const status = runArkCheck(gateArgs, { cwd: root });
|
|
546
|
+
if (status !== 0) return status;
|
|
531
547
|
}
|
|
532
548
|
|
|
533
549
|
// 6) Show the plan: what's safe to auto-fix vs what needs a decision.
|
|
@@ -664,7 +680,7 @@ async function start(args) {
|
|
|
664
680
|
console.log(' → explore first, dual plan (remediation + pattern bets), safe fixes, leave gates on.');
|
|
665
681
|
}
|
|
666
682
|
console.log(` 2. Status anytime: ${arkCommand(root, 'ark-check', '--doctor')}`);
|
|
667
|
-
console.log(` 3. After edits: ${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-
|
|
683
|
+
console.log(` 3. After edits: ${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-merge')}`);
|
|
668
684
|
if (mode === 'adapt' && planOk && !falseGreenGap) {
|
|
669
685
|
console.log(
|
|
670
686
|
` 4. When green but cores still optional: ${arkCommand(root, 'ark-check', '--ratchet-cores')} → honest ENFORCE`
|
|
@@ -696,6 +712,25 @@ async function main() {
|
|
|
696
712
|
return 0;
|
|
697
713
|
}
|
|
698
714
|
|
|
715
|
+
if (args.requireWriteHook && !['start', 'init'].includes(args.command)) {
|
|
716
|
+
console.error('--require-write-hook is supported by ark start and ark init.');
|
|
717
|
+
return 2;
|
|
718
|
+
}
|
|
719
|
+
const enforcement = validateHardWriteRequest({
|
|
720
|
+
root: args.root,
|
|
721
|
+
host: args.requireWriteHook,
|
|
722
|
+
tools: args.tools,
|
|
723
|
+
force: args.force,
|
|
724
|
+
});
|
|
725
|
+
if (!enforcement.ok) {
|
|
726
|
+
console.error(enforcement.error);
|
|
727
|
+
return 2;
|
|
728
|
+
}
|
|
729
|
+
if (enforcement.host) {
|
|
730
|
+
args.requireWriteHook = enforcement.host;
|
|
731
|
+
if (!args.tools) args.tools = enforcement.tools.join(',');
|
|
732
|
+
}
|
|
733
|
+
|
|
699
734
|
if (args.command === 'start') {
|
|
700
735
|
try {
|
|
701
736
|
return await start(args);
|
package/bin/lib/agent-gates.mjs
CHANGED
|
@@ -76,6 +76,8 @@ export {
|
|
|
76
76
|
normalizeToolsList,
|
|
77
77
|
resolveTools,
|
|
78
78
|
KNOWN_TOOLS,
|
|
79
|
+
detectActiveAgentHost,
|
|
80
|
+
codexConcernIsActive,
|
|
79
81
|
arkPackageVersion,
|
|
80
82
|
stampSkill,
|
|
81
83
|
installedSkillVersion,
|
|
@@ -95,6 +97,16 @@ export {
|
|
|
95
97
|
collectAdoptionGaps,
|
|
96
98
|
} from './mcp-adoption.mjs';
|
|
97
99
|
|
|
100
|
+
export {
|
|
101
|
+
detectPreCommitArk,
|
|
102
|
+
detectCiEnforcement,
|
|
103
|
+
detectConfigGateDrift,
|
|
104
|
+
jobIdsThatRunArkCheck,
|
|
105
|
+
isArkRequiredStatusCheck,
|
|
106
|
+
reportGithubBranchProtection,
|
|
107
|
+
collectWeakestLinkGaps,
|
|
108
|
+
} from './weakest-link.mjs';
|
|
109
|
+
|
|
98
110
|
export {
|
|
99
111
|
staleRunnerGateFiles,
|
|
100
112
|
warnLockfileConflict,
|
|
@@ -92,6 +92,14 @@ export function scanSourceFile(ts, root, config, rules, manifestIntentLayers, fi
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
if (
|
|
96
|
+
ts.isImportEqualsDeclaration(node) &&
|
|
97
|
+
ts.isExternalModuleReference(node.moduleReference)
|
|
98
|
+
) {
|
|
99
|
+
const specifier = stringLiteralText(ts, node.moduleReference.expression);
|
|
100
|
+
if (specifier) checkModuleEdge(specifier, node, 'require');
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
if (ts.isCallExpression(node)) {
|
|
96
104
|
const moduleCall = moduleSpecifierFromCall(ts, node);
|
|
97
105
|
if (moduleCall) {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
DEFAULT_LAYER_DIRECTORIES,
|
|
13
13
|
} from '../ark-shared.mjs';
|
|
14
14
|
import { falseGreenAdoptionGap } from './field-install.mjs';
|
|
15
|
+
import { renderHostSupportMatrixMarkdown } from './host-support-matrix.mjs';
|
|
15
16
|
import { PREFERRED_MCP_BIN } from './hook-templates.mjs';
|
|
16
17
|
import { readPackageJson } from './gate-files.mjs';
|
|
17
18
|
|
|
@@ -30,7 +31,7 @@ export function checkArgsForRoot(root, { requireGates = false } = {}) {
|
|
|
30
31
|
const baselineFlag = fs.existsSync(path.join(root, '.ark-baseline.json'))
|
|
31
32
|
? ' --baseline .ark-baseline.json'
|
|
32
33
|
: '';
|
|
33
|
-
const profile = requireGates ? '--strict' : '--strict-config';
|
|
34
|
+
const profile = requireGates ? '--strict-merge' : '--strict-config';
|
|
34
35
|
return `--root . --config ark.config.json ${profile}${baselineFlag}`;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -140,6 +141,10 @@ export function agentInstructions(root) {
|
|
|
140
141
|
Skills are **dual-engine**: deterministic CLI sensors + exploratory read of *this* repo — not JSON-only wrappers.
|
|
141
142
|
When a skill says **STOP — do not continue this skill as complete**, stop and invoke the named handoff skill.
|
|
142
143
|
|
|
144
|
+
## Host enforcement support
|
|
145
|
+
|
|
146
|
+
${renderHostSupportMatrixMarkdown()}
|
|
147
|
+
|
|
143
148
|
### Subagent fan-out
|
|
144
149
|
If the host supports **parallel subagents**, skills may ask you to fan out **read-only**
|
|
145
150
|
scouts (disjoint path scopes) and merge in the parent. If the host does **not**,
|
|
@@ -177,8 +182,9 @@ an ungoverned location:
|
|
|
177
182
|
|
|
178
183
|
${layerPlacementTable()}
|
|
179
184
|
|
|
180
|
-
The project is only considered Ark-enforced when
|
|
181
|
-
|
|
185
|
+
The project is only considered Ark-enforced when its host-appropriate write path is configured
|
|
186
|
+
and the CI check passes. Only Claude/Grok provide a hard local write boundary; Cursor/Codex use
|
|
187
|
+
advisory MCP plus CI. The experimental runtime is not required.
|
|
182
188
|
`;
|
|
183
189
|
}
|
|
184
190
|
|
package/bin/lib/codex-home.mjs
CHANGED
|
@@ -22,6 +22,13 @@ export function codexConfigPath() {
|
|
|
22
22
|
return path.join(base, 'config.toml');
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** True when CODEX_HOME is unset/empty or resolves to the real default ~/.codex. */
|
|
26
|
+
export function usesDefaultCodexHome(env = process.env, homeDir = os.homedir()) {
|
|
27
|
+
const configured = env?.CODEX_HOME;
|
|
28
|
+
if (typeof configured !== 'string' || configured.trim() === '') return true;
|
|
29
|
+
return path.resolve(configured) === path.resolve(homeDir, '.codex');
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
/** Temp / upgrade sandbox roots must never remain as Codex MCP --root. */
|
|
26
33
|
export function isTempOrUpgradeRoot(p) {
|
|
27
34
|
if (!p || typeof p !== 'string') return false;
|