arkgate 2.12.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +122 -0
- package/README.md +90 -51
- package/bin/ark-check.mjs +156 -39
- package/bin/ark-mcp.mjs +119 -6
- package/bin/ark-shared.mjs +216 -129
- package/bin/ark.mjs +134 -34
- package/bin/lib/adapter-contract.mjs +93 -0
- package/bin/lib/agent-gates.mjs +13 -0
- package/bin/lib/analysis-engine.mjs +1171 -0
- package/bin/lib/architecture-scan.mjs +84 -127
- package/bin/lib/ci-and-commands.mjs +40 -3
- package/bin/lib/codex-home.mjs +7 -0
- package/bin/lib/config-contract.mjs +331 -0
- package/bin/lib/config-warnings.mjs +7 -205
- package/bin/lib/doctor-plan.mjs +43 -16
- package/bin/lib/enforcement-profiles.mjs +97 -0
- package/bin/lib/field-install.mjs +67 -10
- package/bin/lib/gate-files.mjs +42 -3
- package/bin/lib/graph-cycles.mjs +4 -54
- package/bin/lib/hook-templates.mjs +33 -1
- package/bin/lib/host-support-matrix.mjs +83 -0
- package/bin/lib/install-migrate.mjs +99 -30
- package/bin/lib/mcp-adoption.mjs +35 -3
- package/bin/lib/open-html.mjs +75 -0
- package/bin/lib/presets.mjs +45 -4
- package/bin/lib/safety-diagnostics.mjs +36 -15
- package/bin/lib/scan-files.mjs +12 -1
- package/bin/lib/skill-install.mjs +72 -1
- package/bin/lib/source-policy.mjs +36 -0
- package/bin/lib/start-preview.mjs +271 -0
- package/bin/lib/ts-resolve.mjs +13 -3
- package/bin/lib/weakest-link.mjs +417 -0
- package/bin/lib/write-path-capabilities.mjs +186 -0
- package/bin/lib/write-path-detect.mjs +62 -99
- package/compat/nestjs.cjs +2 -0
- package/compat/nestjs.d.ts +2 -0
- package/compat/nestjs.js +1 -0
- package/compat/runtime.cjs +2 -0
- package/compat/runtime.d.ts +2 -0
- package/compat/runtime.js +1 -0
- package/dist/configContract-BxSIwVRo.d.cts +259 -0
- package/dist/configContract-BxSIwVRo.d.ts +259 -0
- package/dist/eslint/index.cjs +500 -61
- package/dist/eslint/index.d.cts +36 -20
- package/dist/eslint/index.d.ts +36 -20
- package/dist/eslint/index.js +500 -61
- package/dist/index.cjs +1349 -2741
- package/dist/index.d.cts +449 -483
- package/dist/index.d.ts +449 -483
- package/dist/index.js +1325 -2687
- package/docs/agent-guide.md +58 -34
- package/docs/ai-gates.md +79 -21
- package/docs/configuration.md +97 -0
- package/docs/enthusiast/README.md +3 -3
- package/docs/enthusiast/how-to-agent-gates.md +7 -3
- package/docs/migrate-from-ark-runtime-kernel.md +5 -3
- package/docs/package-surface.md +19 -19
- package/docs/production-hardening.md +31 -5
- package/docs/threat-model.md +65 -0
- package/docs/typescript-support.md +30 -3
- package/package.json +46 -11
- package/schemas/ark.analysis-result.schema.json +91 -0
- package/schemas/ark.config.schema.json +750 -0
- package/server.json +2 -2
- package/templates/hooks/pre-commit-ark +37 -0
- package/templates/skills/ark-architect.md +3 -2
- package/templates/skills/ark-coverage.md +2 -2
- package/templates/skills/ark-runtime.md +8 -5
- package/templates/skills/ark-upgrade.md +36 -16
- package/tests/fixtures/ts-consumer/ark.config.json +2 -0
- package/dist/eslint/index.cjs.map +0 -1
- package/dist/eslint/index.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/nestjs/index.cjs +0 -2498
- package/dist/nestjs/index.cjs.map +0 -1
- package/dist/nestjs/index.d.cts +0 -22
- package/dist/nestjs/index.d.ts +0 -22
- package/dist/nestjs/index.js +0 -2474
- package/dist/nestjs/index.js.map +0 -1
- package/dist/runtime/index.cjs +0 -3352
- package/dist/runtime/index.cjs.map +0 -1
- package/dist/runtime/index.d.cts +0 -2
- package/dist/runtime/index.d.ts +0 -2
- package/dist/runtime/index.js +0 -3270
- package/dist/runtime/index.js.map +0 -1
- package/dist/types-BZ17b9i5.d.cts +0 -1068
- package/dist/types-BZ17b9i5.d.ts +0 -1068
|
@@ -5,16 +5,13 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import {
|
|
8
|
-
collectForbiddenGlobalUses,
|
|
9
8
|
layerForFile,
|
|
10
9
|
looksLikeIntent,
|
|
11
10
|
} from '../ark-shared.mjs';
|
|
12
11
|
import {
|
|
13
|
-
isTypeOnlyModuleReference,
|
|
14
12
|
isArkPublishCandidate,
|
|
15
13
|
isPublishCall,
|
|
16
14
|
lineOf,
|
|
17
|
-
moduleSpecifierFromCall,
|
|
18
15
|
namedModuleBindings,
|
|
19
16
|
objectHasProperty,
|
|
20
17
|
publishHasSource,
|
|
@@ -22,7 +19,6 @@ import {
|
|
|
22
19
|
sourceFileExportsOnlyTypes,
|
|
23
20
|
sourceFileHasTopLevelSideEffects,
|
|
24
21
|
stringLiteralText,
|
|
25
|
-
textOfModuleSpecifier,
|
|
26
22
|
typeOnlyExportNames,
|
|
27
23
|
} from './ast-scan.mjs';
|
|
28
24
|
import { provePortProofInject } from './port-proof.mjs';
|
|
@@ -32,9 +28,14 @@ import {
|
|
|
32
28
|
isBlocked,
|
|
33
29
|
collectConfigWarnings,
|
|
34
30
|
} from './config-warnings.mjs';
|
|
35
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
collectForbiddenCapabilityUses,
|
|
33
|
+
evaluateArchitectureGraph,
|
|
34
|
+
extractSemanticDependencies,
|
|
35
|
+
} from './analysis-engine.mjs';
|
|
36
36
|
import { normalize } from './scan-files.mjs';
|
|
37
37
|
import { collectSafetyDiagnostics } from './safety-diagnostics.mjs';
|
|
38
|
+
import { classifyPublishFacts } from './source-policy.mjs';
|
|
38
39
|
import {
|
|
39
40
|
createCompilerOptionsLookup,
|
|
40
41
|
createModuleResolutionHost,
|
|
@@ -57,11 +58,11 @@ export function scanSourceFile(ts, root, config, rules, manifestIntentLayers, fi
|
|
|
57
58
|
const forbiddenGlobals = Array.isArray(layerConfig?.forbiddenGlobals)
|
|
58
59
|
? layerConfig.forbiddenGlobals.filter((entry) => typeof entry === 'string')
|
|
59
60
|
: [];
|
|
60
|
-
for (const use of
|
|
61
|
+
for (const use of collectForbiddenCapabilityUses(ts, sourceFile, forbiddenGlobals)) {
|
|
61
62
|
violations.push({
|
|
62
63
|
ruleId: 'FORBIDDEN_GLOBAL',
|
|
63
64
|
file: normalize(path.relative(root, file)),
|
|
64
|
-
line:
|
|
65
|
+
line: use.line,
|
|
65
66
|
fromLayer: sourceLayer,
|
|
66
67
|
target: use.name,
|
|
67
68
|
message: `${sourceLayer} must not use the ambient global "${use.name}".`,
|
|
@@ -79,48 +80,37 @@ export function scanSourceFile(ts, root, config, rules, manifestIntentLayers, fi
|
|
|
79
80
|
});
|
|
80
81
|
};
|
|
81
82
|
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
83
|
+
for (const dependency of extractSemanticDependencies(ts, sourceFile)) {
|
|
84
|
+
if (!dependency.specifier) continue;
|
|
85
|
+
checkModuleEdge(
|
|
86
|
+
dependency.specifier,
|
|
87
|
+
dependency.node,
|
|
88
|
+
dependency.kind,
|
|
89
|
+
dependency.typeOnly
|
|
90
|
+
);
|
|
91
|
+
}
|
|
94
92
|
|
|
93
|
+
const needsPolicyWalk =
|
|
94
|
+
/\bpublish\s*\(|\bintent\b/.test(source) ||
|
|
95
|
+
/['"`]\s*[A-Z][A-Za-z0-9_]*\./.test(source);
|
|
96
|
+
const visit = (node) => {
|
|
95
97
|
if (ts.isCallExpression(node)) {
|
|
96
|
-
const moduleCall = moduleSpecifierFromCall(ts, node);
|
|
97
|
-
if (moduleCall) {
|
|
98
|
-
checkModuleEdge(moduleCall.value, node, moduleCall.kind);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
98
|
if (isPublishCall(ts, node)) {
|
|
102
99
|
const firstArg = node.arguments[0];
|
|
103
100
|
const rawIntent = stringLiteralText(ts, firstArg);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
for (const finding of classifyPublishFacts({
|
|
102
|
+
publishCall: true,
|
|
103
|
+
rawIntentName: rawIntent,
|
|
104
|
+
objectHasIntent: objectHasProperty(ts, firstArg, 'intent'),
|
|
105
|
+
arkPublishCandidate: isArkPublishCandidate(ts, node),
|
|
106
|
+
hasSource: publishHasSource(ts, node),
|
|
107
|
+
})) {
|
|
108
108
|
violations.push({
|
|
109
|
-
ruleId:
|
|
109
|
+
ruleId: finding.ruleId,
|
|
110
110
|
file: normalize(path.relative(root, file)),
|
|
111
111
|
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (isArkPublishCandidate(ts, node) && !publishHasSource(ts, node)) {
|
|
118
|
-
violations.push({
|
|
119
|
-
ruleId: 'PUBLISH_MISSING_SOURCE',
|
|
120
|
-
file: normalize(path.relative(root, file)),
|
|
121
|
-
line: lineOf(sourceFile, node.getStart(sourceFile)),
|
|
122
|
-
fromLayer: sourceLayer,
|
|
123
|
-
message: 'Strict Ark publish calls must include metadata.source.',
|
|
112
|
+
...(finding.ruleId === 'PUBLISH_MISSING_SOURCE' ? { fromLayer: sourceLayer } : {}),
|
|
113
|
+
message: finding.message,
|
|
124
114
|
});
|
|
125
115
|
}
|
|
126
116
|
|
|
@@ -167,7 +157,7 @@ export function scanSourceFile(ts, root, config, rules, manifestIntentLayers, fi
|
|
|
167
157
|
|
|
168
158
|
ts.forEachChild(node, visit);
|
|
169
159
|
};
|
|
170
|
-
visit(sourceFile);
|
|
160
|
+
if (needsPolicyWalk) visit(sourceFile);
|
|
171
161
|
return {
|
|
172
162
|
contentViolations: violations,
|
|
173
163
|
edges,
|
|
@@ -186,7 +176,6 @@ export function runArchitectureScan({ root, config, manifest, rules, files, ts,
|
|
|
186
176
|
const compilerOptionsFor = createCompilerOptionsLookup(ts, root, args.tsconfig);
|
|
187
177
|
const moduleHost = createModuleResolutionHost(ts);
|
|
188
178
|
|
|
189
|
-
const violations = [];
|
|
190
179
|
const warnings = collectConfigWarnings(root, config, files, rules, manifest);
|
|
191
180
|
const safety = collectSafetyDiagnostics(ts, root, config, files);
|
|
192
181
|
warnings.push(...safety.warnings);
|
|
@@ -194,13 +183,11 @@ export function runArchitectureScan({ root, config, manifest, rules, files, ts,
|
|
|
194
183
|
const cachedFiles = cacheKey ? loadScanCache(root, cacheKey) : undefined;
|
|
195
184
|
const nextCacheFiles = {};
|
|
196
185
|
|
|
197
|
-
const importGraph = new Map();
|
|
198
186
|
const scanned = [];
|
|
199
187
|
for (const file of files) {
|
|
200
188
|
const sourceLayer = layerForFile(root, file, config.layers);
|
|
201
189
|
if (!sourceLayer) continue;
|
|
202
190
|
const relFile = normalize(path.relative(root, file));
|
|
203
|
-
if (!importGraph.has(relFile)) importGraph.set(relFile, new Set());
|
|
204
191
|
const stat = fs.statSync(file);
|
|
205
192
|
const fileKey = `${stat.mtimeMs}:${stat.size}`;
|
|
206
193
|
const cached = cachedFiles?.[relFile];
|
|
@@ -223,8 +210,8 @@ export function runArchitectureScan({ root, config, manifest, rules, files, ts,
|
|
|
223
210
|
scanned.push({ file, sourceLayer, relFile, entry });
|
|
224
211
|
}
|
|
225
212
|
|
|
213
|
+
const engineEdges = [];
|
|
226
214
|
for (const { file, sourceLayer, relFile, entry } of scanned) {
|
|
227
|
-
violations.push(...entry.contentViolations);
|
|
228
215
|
for (const edge of entry.edges) {
|
|
229
216
|
const target = resolveImport(
|
|
230
217
|
ts,
|
|
@@ -235,99 +222,69 @@ export function runArchitectureScan({ root, config, manifest, rules, files, ts,
|
|
|
235
222
|
root
|
|
236
223
|
);
|
|
237
224
|
const targetLayer = target ? layerForFile(root, target, config.layers) : undefined;
|
|
238
|
-
if (target && targetLayer) {
|
|
239
|
-
const relTarget = normalize(path.relative(root, target));
|
|
240
|
-
if (relTarget !== relFile && !edge.typeOnly) {
|
|
241
|
-
importGraph.get(relFile).add(relTarget);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
225
|
const relTarget = target ? normalize(path.relative(root, target)) : undefined;
|
|
245
|
-
const
|
|
226
|
+
const targetCached = relTarget ? nextCacheFiles[relTarget] : undefined;
|
|
227
|
+
const staticEdge = edge.kind === 'import' || edge.kind === 'export';
|
|
228
|
+
const targetTypeOnlyExports =
|
|
229
|
+
staticEdge && Boolean(targetCached?.exportsOnlyTypes) && !edge.typeOnly;
|
|
230
|
+
const sourcePureTypeModule = Boolean(entry.exportsOnlyTypes);
|
|
231
|
+
const targetTypeNames = new Set(targetCached?.typeOnlyExportNames || []);
|
|
232
|
+
const named = edge.namedBindings;
|
|
233
|
+
const namedBindingsTypeOnly =
|
|
234
|
+
staticEdge &&
|
|
235
|
+
Array.isArray(named) &&
|
|
236
|
+
named.length > 0 &&
|
|
237
|
+
targetTypeNames.size > 0 &&
|
|
238
|
+
!targetCached?.hasTopLevelSideEffects &&
|
|
239
|
+
named.every((name) => targetTypeNames.has(name));
|
|
240
|
+
const deniedRule = targetLayer
|
|
246
241
|
? isBlocked(rules, sourceLayer, targetLayer, {
|
|
247
242
|
fromPath: relFile,
|
|
248
243
|
toPath: relTarget,
|
|
249
244
|
layers: config.layers,
|
|
250
245
|
})
|
|
251
246
|
: undefined;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
named.length > 0 &&
|
|
267
|
-
targetTypeNames.size > 0 &&
|
|
268
|
-
!targetCached?.hasTopLevelSideEffects &&
|
|
269
|
-
named.every((n) => targetTypeNames.has(n));
|
|
270
|
-
const peerIsolation = Boolean(rule.peerIsolation);
|
|
271
|
-
// W6: port-proof eligibility (value import only; fail-closed static proof).
|
|
272
|
-
let portProofEligible = false;
|
|
273
|
-
if (
|
|
274
|
-
!edge.typeOnly &&
|
|
275
|
-
!peerIsolation &&
|
|
276
|
-
edge.kind === 'import' &&
|
|
277
|
-
!targetTypeOnlyExports &&
|
|
278
|
-
!namedBindingsTypeOnly
|
|
279
|
-
) {
|
|
280
|
-
try {
|
|
281
|
-
const srcText = fs.readFileSync(file, 'utf8');
|
|
282
|
-
const proof = provePortProofInject(ts, srcText, { filePath: file });
|
|
283
|
-
portProofEligible = Boolean(proof.eligible);
|
|
284
|
-
} catch {
|
|
285
|
-
portProofEligible = false;
|
|
286
|
-
}
|
|
247
|
+
let portProofEligible = false;
|
|
248
|
+
if (
|
|
249
|
+
deniedRule &&
|
|
250
|
+
!deniedRule.peerIsolation &&
|
|
251
|
+
!edge.typeOnly &&
|
|
252
|
+
edge.kind === 'import' &&
|
|
253
|
+
!targetTypeOnlyExports &&
|
|
254
|
+
!namedBindingsTypeOnly
|
|
255
|
+
) {
|
|
256
|
+
try {
|
|
257
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
258
|
+
portProofEligible = Boolean(provePortProofInject(ts, source, { filePath: file }).eligible);
|
|
259
|
+
} catch {
|
|
260
|
+
portProofEligible = false;
|
|
287
261
|
}
|
|
288
|
-
violations.push({
|
|
289
|
-
ruleId: 'LAYER_IMPORT_VIOLATION',
|
|
290
|
-
file: relFile,
|
|
291
|
-
line: edge.line,
|
|
292
|
-
fromLayer: sourceLayer,
|
|
293
|
-
toLayer: targetLayer,
|
|
294
|
-
target: relTarget,
|
|
295
|
-
...(edge.typeOnly ? { typeOnly: true } : {}),
|
|
296
|
-
...(targetTypeOnlyExports ? { targetTypeOnlyExports: true } : {}),
|
|
297
|
-
...(sourcePureTypeModule ? { sourcePureTypeModule: true } : {}),
|
|
298
|
-
...(namedBindingsTypeOnly ? { namedBindingsTypeOnly: true } : {}),
|
|
299
|
-
...(portProofEligible ? { portProofEligible: true } : {}),
|
|
300
|
-
...(edge.kind ? { edgeKind: edge.kind } : {}),
|
|
301
|
-
...(peerIsolation ? { peerIsolation: true } : {}),
|
|
302
|
-
message:
|
|
303
|
-
rule.message ??
|
|
304
|
-
(peerIsolation
|
|
305
|
-
? `${sourceLayer} must not ${edge.kind} another slice of ${targetLayer} (${relFile} → ${relTarget}). Extract shared code or use events/ports across slices.`
|
|
306
|
-
: `${sourceLayer} must not ${edge.kind} ${targetLayer}.`),
|
|
307
|
-
});
|
|
308
262
|
}
|
|
263
|
+
engineEdges.push({
|
|
264
|
+
from: relFile,
|
|
265
|
+
fromLayer: sourceLayer,
|
|
266
|
+
to: relTarget,
|
|
267
|
+
toLayer: targetLayer,
|
|
268
|
+
line: edge.line,
|
|
269
|
+
kind: edge.kind,
|
|
270
|
+
typeOnly: edge.typeOnly,
|
|
271
|
+
targetTypeOnlyExports,
|
|
272
|
+
sourcePureTypeModule,
|
|
273
|
+
namedBindingsTypeOnly,
|
|
274
|
+
portProofEligible,
|
|
275
|
+
});
|
|
309
276
|
}
|
|
310
277
|
}
|
|
311
278
|
|
|
312
279
|
if (cacheKey) saveScanCache(root, cacheKey, nextCacheFiles);
|
|
313
280
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
target: c.target,
|
|
324
|
-
failsStrict: false,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
violations.push(...cycles);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return { violations, warnings, safety: safety.report };
|
|
281
|
+
return evaluateArchitectureGraph({
|
|
282
|
+
config,
|
|
283
|
+
rules,
|
|
284
|
+
files: scanned.map(({ relFile }) => relFile),
|
|
285
|
+
contentViolations: scanned.flatMap(({ entry }) => entry.contentViolations),
|
|
286
|
+
edges: engineEdges,
|
|
287
|
+
warnings,
|
|
288
|
+
safety: safety.report,
|
|
289
|
+
});
|
|
333
290
|
}
|
|
@@ -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,40 @@ 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.
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Compact onboarding uses one project router instead of copied slash-command
|
|
193
|
+
* skills. The package and ark MCP resources remain the canonical capability
|
|
194
|
+
* source; the marker makes the selected host verifiable by the strict gate.
|
|
195
|
+
*/
|
|
196
|
+
export function compactAgentInstructions(root, host = null) {
|
|
197
|
+
const selectedHost = host || 'none';
|
|
198
|
+
const checkCmd = arkCheckCommand(root);
|
|
199
|
+
const doctorCmd = arkCommand(root, 'ark-check', '--doctor');
|
|
200
|
+
const installSkills = arkCommand(
|
|
201
|
+
root,
|
|
202
|
+
'ark-check',
|
|
203
|
+
`--install-agent-gates --skills-only --tools ${selectedHost === 'none' ? '<host>' : selectedHost}`
|
|
204
|
+
);
|
|
205
|
+
return `# Ark Enforcement
|
|
206
|
+
|
|
207
|
+
<!-- arkgate:compact-router host=${selectedHost} -->
|
|
208
|
+
## Compact router
|
|
209
|
+
|
|
210
|
+
This project uses the ArkGate package and its \`ark\` MCP resources as its one
|
|
211
|
+
agent router. Before editing TypeScript or JavaScript, read \`ark://manifest\`
|
|
212
|
+
when available; use \`ark_place\` for new files and \`validate_code\` after edits.
|
|
213
|
+
If MCP is unavailable, inspect \`ark.config.json\` and run \`${checkCmd}\`.
|
|
214
|
+
|
|
215
|
+
For architecture status, run \`${doctorCmd}\`. The selected host is
|
|
216
|
+
\`${selectedHost}\`; its host registration and CI gate are installed alongside
|
|
217
|
+
this file. Full \`/ark-*\` guided workflows are optional and can be added later
|
|
218
|
+
with \`${installSkills}\`.
|
|
182
219
|
`;
|
|
183
220
|
}
|
|
184
221
|
|
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;
|