arkgate 3.0.4 → 3.1.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 +82 -1
- package/README.md +29 -9
- package/bin/ark-check.mjs +69 -54
- package/bin/ark-mcp.mjs +267 -26
- package/bin/ark.mjs +50 -3
- package/bin/lib/adapter-contract.mjs +27 -1
- package/bin/lib/agent-gates.mjs +9 -0
- package/bin/lib/analysis-engine.mjs +7 -1169
- package/bin/lib/ci-and-commands.mjs +4 -0
- package/bin/lib/codex-home.mjs +10 -1
- package/bin/lib/doctor-plan.mjs +37 -9
- package/bin/lib/host-support-matrix.mjs +6 -2
- package/bin/lib/install-migrate.mjs +81 -25
- package/bin/lib/mcp-adoption.mjs +8 -0
- package/bin/lib/policy-delta-io.mjs +161 -0
- package/bin/lib/prepare-change.mjs +186 -0
- package/bin/lib/remediation.mjs +24 -0
- package/bin/lib/skill-install.mjs +302 -22
- package/bin/lib/violations.mjs +2 -2
- package/bin/lib/weakest-link.mjs +61 -12
- package/bin/lib/write-path-capabilities.mjs +70 -2
- package/bin/lib/write-path-detect.mjs +18 -11
- package/dist/eslint/index.cjs +3 -977
- package/dist/eslint/index.js +3 -931
- package/dist/index.cjs +6 -1960
- package/dist/index.d.cts +152 -5
- package/dist/index.d.ts +152 -5
- package/dist/index.js +6 -1908
- package/docs/agent-guide.md +16 -2
- package/docs/ai-gates.md +35 -3
- package/docs/configuration.md +44 -0
- package/docs/package-surface.md +8 -1
- package/docs/threat-model.md +7 -4
- package/package.json +6 -5
- package/schemas/ark.analysis-result.schema.json +5 -1
- package/schemas/ark.change-map.schema.json +77 -0
- package/server.json +2 -2
- package/templates/skills/ark-upgrade.md +9 -5
- package/docs/ark-check-example.json +0 -87
- package/docs/demos/03-copilot-autopilot.md +0 -93
- package/docs/migrate-from-ark-runtime-kernel.md +0 -174
- package/docs/production-hardening.md +0 -100
package/bin/lib/weakest-link.mjs
CHANGED
|
@@ -48,6 +48,30 @@ export function detectPreCommitArk(root) {
|
|
|
48
48
|
return { present, arkAware, path: hit };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Classify ark-check flags in a workflow or package script body.
|
|
53
|
+
* CLI: `--strict` and `--strict-merge` both set strictConfig + requireGates (fail-closed).
|
|
54
|
+
* `--strict-config` alone does not require gate files.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} text
|
|
57
|
+
* @returns {{ hasFailClosedFlag: boolean, hasStrictConfigOnly: boolean, hasStrictFlag: boolean }}
|
|
58
|
+
*/
|
|
59
|
+
export function classifyArkCheckFlags(text) {
|
|
60
|
+
if (!text || typeof text !== 'string') {
|
|
61
|
+
return { hasFailClosedFlag: false, hasStrictConfigOnly: false, hasStrictFlag: false };
|
|
62
|
+
}
|
|
63
|
+
const hasStrictMerge = /--strict-merge\b/.test(text);
|
|
64
|
+
const hasRequireGates = /--require-gates\b/.test(text);
|
|
65
|
+
// Bare --strict (alias of --strict-merge), not --strict-config / already-matched merge.
|
|
66
|
+
const withoutLong = text.replace(/--strict-merge\b/g, ' ').replace(/--strict-config\b/g, ' ');
|
|
67
|
+
const hasBareStrict = /--strict\b/.test(withoutLong);
|
|
68
|
+
const hasFailClosedFlag = hasStrictMerge || hasRequireGates || hasBareStrict;
|
|
69
|
+
const hasStrictConfig = /--strict-config\b/.test(text);
|
|
70
|
+
const hasStrictConfigOnly = hasStrictConfig && !hasFailClosedFlag;
|
|
71
|
+
const hasStrictFlag = hasFailClosedFlag || hasStrictConfigOnly;
|
|
72
|
+
return { hasFailClosedFlag, hasStrictConfigOnly, hasStrictFlag };
|
|
73
|
+
}
|
|
74
|
+
|
|
51
75
|
/**
|
|
52
76
|
* @param {string} root
|
|
53
77
|
* @returns {{
|
|
@@ -56,6 +80,9 @@ export function detectPreCommitArk(root) {
|
|
|
56
80
|
* arkWorkflowFiles: string[],
|
|
57
81
|
* hasArkCheckWorkflow: boolean,
|
|
58
82
|
* hasStrictFlag: boolean,
|
|
83
|
+
* hasFailClosedFlag: boolean,
|
|
84
|
+
* hasStrictConfigOnly: boolean,
|
|
85
|
+
* failClosed: boolean,
|
|
59
86
|
* hasArchitectureJobName: boolean,
|
|
60
87
|
* }}
|
|
61
88
|
*/
|
|
@@ -67,6 +94,9 @@ export function detectCiEnforcement(root) {
|
|
|
67
94
|
arkWorkflowFiles: [],
|
|
68
95
|
hasArkCheckWorkflow: false,
|
|
69
96
|
hasStrictFlag: false,
|
|
97
|
+
hasFailClosedFlag: false,
|
|
98
|
+
hasStrictConfigOnly: false,
|
|
99
|
+
failClosed: false,
|
|
70
100
|
hasArchitectureJobName: false,
|
|
71
101
|
};
|
|
72
102
|
if (!out.hasWorkflowsDir) return out;
|
|
@@ -77,6 +107,19 @@ export function detectCiEnforcement(root) {
|
|
|
77
107
|
return out;
|
|
78
108
|
}
|
|
79
109
|
out.workflowFiles = files;
|
|
110
|
+
|
|
111
|
+
let checkArchScript = '';
|
|
112
|
+
try {
|
|
113
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
114
|
+
checkArchScript =
|
|
115
|
+
typeof pkg?.scripts?.['check:architecture'] === 'string'
|
|
116
|
+
? pkg.scripts['check:architecture']
|
|
117
|
+
: '';
|
|
118
|
+
} catch {
|
|
119
|
+
checkArchScript = '';
|
|
120
|
+
}
|
|
121
|
+
const scriptFlags = classifyArkCheckFlags(checkArchScript);
|
|
122
|
+
|
|
80
123
|
for (const f of files) {
|
|
81
124
|
let text = '';
|
|
82
125
|
try {
|
|
@@ -92,9 +135,19 @@ export function detectCiEnforcement(root) {
|
|
|
92
135
|
if (mentionsArk) {
|
|
93
136
|
out.hasArkCheckWorkflow = true;
|
|
94
137
|
out.arkWorkflowFiles.push(`.github/workflows/${f}`);
|
|
95
|
-
|
|
138
|
+
const flags = classifyArkCheckFlags(text);
|
|
139
|
+
const viaScript = /check:architecture/.test(text) && scriptFlags.hasFailClosedFlag;
|
|
140
|
+
if (flags.hasFailClosedFlag || viaScript) {
|
|
141
|
+
out.hasFailClosedFlag = true;
|
|
142
|
+
out.failClosed = true;
|
|
143
|
+
out.hasStrictFlag = true;
|
|
144
|
+
} else if (flags.hasStrictConfigOnly) {
|
|
145
|
+
out.hasStrictConfigOnly = true;
|
|
146
|
+
out.hasStrictFlag = true;
|
|
147
|
+
} else if (flags.hasStrictFlag) {
|
|
96
148
|
out.hasStrictFlag = true;
|
|
97
149
|
}
|
|
150
|
+
// check:architecture without fail-closed flags in the script is NOT fail-closed.
|
|
98
151
|
if (/architecture|ark-check|arkgate-check/i.test(f) || /name:\s*.*ark/i.test(text)) {
|
|
99
152
|
out.hasArchitectureJobName = true;
|
|
100
153
|
}
|
|
@@ -339,18 +392,14 @@ export function collectWeakestLinkGaps(root, opts = {}) {
|
|
|
339
392
|
'CI workflows exist but none run ark-check / arkgate-check / check:architecture',
|
|
340
393
|
fix: arkCommand(root, 'ark-check', '--install-agent-gates'),
|
|
341
394
|
});
|
|
342
|
-
} else if (
|
|
343
|
-
adopted &&
|
|
344
|
-
!isProducer &&
|
|
345
|
-
ci.hasArkCheckWorkflow &&
|
|
346
|
-
!ci.hasStrictFlag
|
|
347
|
-
) {
|
|
395
|
+
} else if (adopted && !isProducer && ci.hasArkCheckWorkflow && !ci.failClosed) {
|
|
348
396
|
gaps.push({
|
|
349
|
-
id: 'enforcement-ci-not-
|
|
350
|
-
severity: '
|
|
351
|
-
message:
|
|
352
|
-
'Architecture CI
|
|
353
|
-
|
|
397
|
+
id: 'enforcement-ci-not-fail-closed',
|
|
398
|
+
severity: 'warn',
|
|
399
|
+
message: ci.hasStrictConfigOnly
|
|
400
|
+
? 'Architecture CI uses --strict-config only (config coverage without gate-file presence). Prefer the fail-closed profile.'
|
|
401
|
+
: 'Architecture CI job found but does not use the fail-closed profile (--strict-merge / --strict / --require-gates)',
|
|
402
|
+
fix: 'ark-check --root . --config ark.config.json --strict-merge --baseline .ark-baseline.json',
|
|
354
403
|
});
|
|
355
404
|
}
|
|
356
405
|
|
|
@@ -25,6 +25,66 @@ export const WRITE_CAPABILITY_NAMES = [
|
|
|
25
25
|
'repair-payload',
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
+
function boundaryState({ supported, evidence, active, bypassable, hard = false, extra = {} }) {
|
|
29
|
+
return {
|
|
30
|
+
supported,
|
|
31
|
+
installed: evidence.length > 0,
|
|
32
|
+
active,
|
|
33
|
+
bypassable,
|
|
34
|
+
hard,
|
|
35
|
+
evidence: [...evidence],
|
|
36
|
+
...extra,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function operationCovered(profile, operation) {
|
|
41
|
+
if (!profile || typeof operation !== 'string') return false;
|
|
42
|
+
const normalized = operation.trim().toLowerCase();
|
|
43
|
+
return profile.hookOperations.some((candidate) => candidate.toLowerCase() === normalized);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildEnforcementLadder(activeHost, support, evidence, attempt) {
|
|
47
|
+
const localInstalled = evidence['hard-write'].length > 0;
|
|
48
|
+
const observedPreTool = attempt?.boundary === 'pre-tool';
|
|
49
|
+
const covered = observedPreTool && operationCovered(support, attempt.operation);
|
|
50
|
+
const hard = Boolean(
|
|
51
|
+
support?.capabilities['hard-write'] && (localInstalled || observedPreTool) && covered
|
|
52
|
+
);
|
|
53
|
+
const inferredActive = (installed) => (installed ? 'unverified' : false);
|
|
54
|
+
return {
|
|
55
|
+
schemaVersion: '1.0',
|
|
56
|
+
activeHost,
|
|
57
|
+
localWrite: boundaryState({
|
|
58
|
+
supported: Boolean(support?.capabilities['hard-write']),
|
|
59
|
+
evidence: evidence['hard-write'],
|
|
60
|
+
active: observedPreTool ? covered : inferredActive(localInstalled),
|
|
61
|
+
bypassable: !hard,
|
|
62
|
+
hard,
|
|
63
|
+
extra: {
|
|
64
|
+
installed: localInstalled || observedPreTool,
|
|
65
|
+
completePatch: Boolean(covered && attempt?.completePatch),
|
|
66
|
+
coverage: covered && attempt?.completePatch ? 'complete-patch' : support?.hookSurface ?? null,
|
|
67
|
+
...(observedPreTool
|
|
68
|
+
? { operation: attempt.operation, operationCovered: covered }
|
|
69
|
+
: { operationCovered: 'unverified' }),
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
advisoryMcp: boundaryState({
|
|
73
|
+
supported: Boolean(support?.capabilities['advisory-write']),
|
|
74
|
+
evidence: evidence['advisory-write'],
|
|
75
|
+
active: inferredActive(evidence['advisory-write'].length > 0),
|
|
76
|
+
bypassable: true,
|
|
77
|
+
}),
|
|
78
|
+
ciMerge: boundaryState({
|
|
79
|
+
supported: true,
|
|
80
|
+
evidence: evidence['merge-gate'],
|
|
81
|
+
active: inferredActive(evidence['merge-gate'].length > 0),
|
|
82
|
+
bypassable: 'unknown',
|
|
83
|
+
extra: { requiredStatus: 'unverified' },
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
28
88
|
const KNOWN_HOSTS = HOST_SUPPORT_HOSTS;
|
|
29
89
|
|
|
30
90
|
function unique(values) {
|
|
@@ -121,7 +181,9 @@ function hostRecord(hard, advisory, repair, merge) {
|
|
|
121
181
|
}
|
|
122
182
|
|
|
123
183
|
export function detectWritePathInventory(root) {
|
|
124
|
-
|
|
184
|
+
// Merge-gate evidence only when CI uses the fail-closed profile (not bare ark-check).
|
|
185
|
+
const ci = detectCiEnforcement(root);
|
|
186
|
+
const merge = ci.failClosed ? ci.arkWorkflowFiles : [];
|
|
125
187
|
const claudeHook = hookEvidence(root, '.claude/settings.json');
|
|
126
188
|
const grokHook = hookEvidence(root, '.grok/hooks/ark-write-gate.json');
|
|
127
189
|
const hosts = {
|
|
@@ -162,7 +224,7 @@ export function detectWritePathInventory(root) {
|
|
|
162
224
|
};
|
|
163
225
|
}
|
|
164
226
|
|
|
165
|
-
export function buildWritePathCapabilityModel(root, explicitHost) {
|
|
227
|
+
export function buildWritePathCapabilityModel(root, explicitHost, attempt) {
|
|
166
228
|
const inventory = detectWritePathInventory(root);
|
|
167
229
|
const detectedHost = explicitHost ?? detectActiveAgentHost();
|
|
168
230
|
const activeHost = KNOWN_HOSTS.includes(detectedHost) ? detectedHost : 'unknown';
|
|
@@ -181,6 +243,12 @@ export function buildWritePathCapabilityModel(root, explicitHost) {
|
|
|
181
243
|
support: getHostSupportProfile(activeHost),
|
|
182
244
|
capabilities: capabilityMap(capabilityEvidence),
|
|
183
245
|
capabilityEvidence,
|
|
246
|
+
enforcementLadder: buildEnforcementLadder(
|
|
247
|
+
activeHost,
|
|
248
|
+
getHostSupportProfile(activeHost),
|
|
249
|
+
capabilityEvidence,
|
|
250
|
+
attempt
|
|
251
|
+
),
|
|
184
252
|
inventory,
|
|
185
253
|
};
|
|
186
254
|
}
|
|
@@ -15,9 +15,9 @@ function installToolsForHost(activeHost) {
|
|
|
15
15
|
: activeHost;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function detectWritePathCapabilities(root, explicitHost) {
|
|
19
|
-
const model = buildWritePathCapabilityModel(root, explicitHost);
|
|
20
|
-
const { activeHost, support, capabilities, capabilityEvidence, inventory } = model;
|
|
18
|
+
export function detectWritePathCapabilities(root, explicitHost, attempt) {
|
|
19
|
+
const model = buildWritePathCapabilityModel(root, explicitHost, attempt);
|
|
20
|
+
const { activeHost, support, capabilities, capabilityEvidence, enforcementLadder, inventory } = model;
|
|
21
21
|
const hardWrite = capabilities['hard-write'];
|
|
22
22
|
const advisoryWrite = capabilities['advisory-write'];
|
|
23
23
|
const repairPayload = capabilities['repair-payload'];
|
|
@@ -72,17 +72,23 @@ export function detectWritePathCapabilities(root, explicitHost) {
|
|
|
72
72
|
),
|
|
73
73
|
};
|
|
74
74
|
} else if (mode === 'mcp-only') {
|
|
75
|
+
const codexHonesty =
|
|
76
|
+
activeHost === 'codex'
|
|
77
|
+
? 'Codex local write is advisory (MCP + best-effort hooks.json — not a hard boundary; ' +
|
|
78
|
+
'not equivalent to Claude/Grok PreToolUse hard-write + repair). ' +
|
|
79
|
+
'The hard merge backstop is CI --strict-merge plus a required status check.'
|
|
80
|
+
: `Active host ${activeHost} has advisory prepare-write/autoPatch tools, ` +
|
|
81
|
+
'but no hard write boundary; the CI check can still reject the change before merge.';
|
|
75
82
|
gap = {
|
|
76
83
|
id: 'write-path-mcp-only',
|
|
77
84
|
severity: 'info',
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
),
|
|
85
|
+
host: activeHost,
|
|
86
|
+
message: codexHonesty,
|
|
87
|
+
fix:
|
|
88
|
+
activeHost === 'codex'
|
|
89
|
+
? 'Keep CI on --strict-merge and require the ark-check status on the default branch; ' +
|
|
90
|
+
`refresh Codex MCP/skills with ${arkCommand(root, 'ark-check', '--install-agent-gates --tools codex')}`
|
|
91
|
+
: arkCommand(root, 'ark-check', `--install-agent-gates --tools ${tools}`),
|
|
86
92
|
};
|
|
87
93
|
}
|
|
88
94
|
|
|
@@ -92,6 +98,7 @@ export function detectWritePathCapabilities(root, explicitHost) {
|
|
|
92
98
|
supportSummary: formatHostSupportSummary(support),
|
|
93
99
|
capabilities,
|
|
94
100
|
capabilityEvidence,
|
|
101
|
+
enforcementLadder,
|
|
95
102
|
inventory,
|
|
96
103
|
// Compatibility projection for existing doctor/API consumers.
|
|
97
104
|
mode,
|