@nerviq/cli 1.8.8 → 1.8.9
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/bin/cli.js +58 -4
- package/package.json +1 -1
- package/src/benchmark.js +7 -3
- package/src/certification.js +32 -4
- package/src/governance.js +2 -2
- package/src/mcp-server.js +1 -1
- package/src/server.js +14 -5
- package/src/setup.js +56 -3
package/bin/cli.js
CHANGED
|
@@ -308,6 +308,7 @@ function printScanDetail(summary, options) {
|
|
|
308
308
|
|
|
309
309
|
// Show per-category breakdown if result is available
|
|
310
310
|
if (item.result && item.result.results) {
|
|
311
|
+
const STACK_LANGUAGES = new Set(['python', 'go', 'rust', 'java', 'ruby', 'dotnet', 'php', 'flutter', 'swift', 'kotlin']);
|
|
311
312
|
const categories = {};
|
|
312
313
|
for (const r of item.result.results) {
|
|
313
314
|
const cat = r.category || 'other';
|
|
@@ -315,7 +316,9 @@ function printScanDetail(summary, options) {
|
|
|
315
316
|
categories[cat].total++;
|
|
316
317
|
if (r.passed) categories[cat].passed++;
|
|
317
318
|
}
|
|
318
|
-
const catEntries = Object.entries(categories)
|
|
319
|
+
const catEntries = Object.entries(categories)
|
|
320
|
+
.filter(([cat, v]) => v.passed > 0 || !STACK_LANGUAGES.has(cat))
|
|
321
|
+
.sort((a, b) => (a[1].passed / a[1].total) - (b[1].passed / b[1].total));
|
|
319
322
|
const catLine = catEntries.map(([cat, v]) => `${cat}: ${v.passed}/${v.total}`).join(' ');
|
|
320
323
|
console.log(` \x1b[2m${catLine}\x1b[0m`);
|
|
321
324
|
}
|
|
@@ -1402,7 +1405,21 @@ async function main() {
|
|
|
1402
1405
|
console.error('\n Error: Profile name required. Usage: nerviq profile load <name>\n');
|
|
1403
1406
|
process.exit(1);
|
|
1404
1407
|
}
|
|
1405
|
-
|
|
1408
|
+
let profile;
|
|
1409
|
+
try {
|
|
1410
|
+
profile = loadProfile(options.dir, profileArg);
|
|
1411
|
+
} catch {
|
|
1412
|
+
// Not found as a user-saved profile — try built-in governance profiles
|
|
1413
|
+
const { getPermissionProfile } = require('../src/governance');
|
|
1414
|
+
const builtIn = getPermissionProfile(profileArg);
|
|
1415
|
+
if (builtIn && builtIn.key === profileArg) {
|
|
1416
|
+
profile = { name: builtIn.label, platforms: ['claude'], threshold: builtIn.threshold || 0, ...builtIn };
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (!profile) {
|
|
1420
|
+
console.error(`\n Error: Profile '${profileArg}' not found. Run 'nerviq profile list' to see available profiles.\n`);
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1406
1423
|
|
|
1407
1424
|
// Apply profile settings to .claude/settings.json
|
|
1408
1425
|
const fs = require('fs');
|
|
@@ -1458,8 +1475,35 @@ async function main() {
|
|
|
1458
1475
|
process.exit(1);
|
|
1459
1476
|
}
|
|
1460
1477
|
} else if (normalizedCommand === 'synergy-report') {
|
|
1461
|
-
|
|
1462
|
-
|
|
1478
|
+
const { formatSynergyReport } = require('../src/synergy/report');
|
|
1479
|
+
const { detectActivePlatforms: detectSynergyPlatforms } = require('../src/harmony/canon');
|
|
1480
|
+
const presentPlatforms = detectSynergyPlatforms(options.dir).map(p => p.platform);
|
|
1481
|
+
if (presentPlatforms.length === 0) {
|
|
1482
|
+
console.log('\n No platform configurations detected.');
|
|
1483
|
+
console.log(' Run "nerviq harmony-audit" first, or "nerviq setup" to bootstrap a platform.\n');
|
|
1484
|
+
process.exit(0);
|
|
1485
|
+
}
|
|
1486
|
+
const platformAudits = {};
|
|
1487
|
+
const activePlatforms = [];
|
|
1488
|
+
for (const plat of presentPlatforms) {
|
|
1489
|
+
try {
|
|
1490
|
+
const result = await audit({ dir: options.dir, silent: true, platform: plat });
|
|
1491
|
+
if (result && typeof result.score === 'number') {
|
|
1492
|
+
platformAudits[plat] = result;
|
|
1493
|
+
activePlatforms.push(plat);
|
|
1494
|
+
}
|
|
1495
|
+
} catch (_e) { /* platform not available */ }
|
|
1496
|
+
}
|
|
1497
|
+
if (activePlatforms.length === 0) {
|
|
1498
|
+
console.log('\n No auditable platforms found. Run "nerviq harmony-audit" first.\n');
|
|
1499
|
+
process.exit(0);
|
|
1500
|
+
}
|
|
1501
|
+
const report = formatSynergyReport({ platformAudits, activePlatforms });
|
|
1502
|
+
if (options.json) {
|
|
1503
|
+
console.log(JSON.stringify({ activePlatforms, platformAudits }, null, 2));
|
|
1504
|
+
} else {
|
|
1505
|
+
console.log(report);
|
|
1506
|
+
}
|
|
1463
1507
|
process.exit(0);
|
|
1464
1508
|
} else if (normalizedCommand === 'doctor') {
|
|
1465
1509
|
const { runDoctor } = require('../src/doctor');
|
|
@@ -1888,6 +1932,16 @@ async function main() {
|
|
|
1888
1932
|
process.exit(0);
|
|
1889
1933
|
}
|
|
1890
1934
|
const result = await audit(options);
|
|
1935
|
+
if (options.out) {
|
|
1936
|
+
const fs = require('fs');
|
|
1937
|
+
const path = require('path');
|
|
1938
|
+
const outPath = path.resolve(options.out);
|
|
1939
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
1940
|
+
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
|
|
1941
|
+
if (!options.json) {
|
|
1942
|
+
console.log(`\n Audit report written to ${options.out}\n`);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1891
1945
|
if (options.webhookUrl) {
|
|
1892
1946
|
try {
|
|
1893
1947
|
const { sendWebhook, formatSlackMessage } = require('../src/integrations');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.9",
|
|
4
4
|
"description": "The intelligent nervous system for AI coding agents — 2,431 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/benchmark.js
CHANGED
|
@@ -321,9 +321,13 @@ function printBenchmark(report, options = {}) {
|
|
|
321
321
|
console.log(' ═══════════════════════════════════════');
|
|
322
322
|
console.log(' Runs in an isolated temp copy. Your current repo is not modified.');
|
|
323
323
|
console.log('');
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
console.log(`
|
|
324
|
+
const orgDeltaSign = report.delta.organicScore >= 0 ? '+' : '';
|
|
325
|
+
const totalDeltaSign = report.delta.score >= 0 ? '+' : '';
|
|
326
|
+
console.log(` Organic improvement: \x1b[1m${orgDeltaSign}${report.delta.organicScore} points\x1b[0m (your actual config quality)`);
|
|
327
|
+
console.log(` Total with nerviq setup: ${totalDeltaSign}${report.delta.score} points`);
|
|
328
|
+
console.log('');
|
|
329
|
+
console.log(` Before: organic ${report.before.organicScore}/100, total ${report.before.score}/100`);
|
|
330
|
+
console.log(` After: organic ${report.after.organicScore}/100, total ${report.after.score}/100`);
|
|
327
331
|
console.log('');
|
|
328
332
|
console.log(` ${report.executiveSummary.headline}`);
|
|
329
333
|
console.log(` Recommendation: ${report.executiveSummary.decisionGuidance}`);
|
package/src/certification.js
CHANGED
|
@@ -37,10 +37,14 @@ async function certifyProject(dir) {
|
|
|
37
37
|
|
|
38
38
|
// Run per-platform audits
|
|
39
39
|
const platformScores = {};
|
|
40
|
+
const allAuditResults = [];
|
|
40
41
|
for (const platform of platforms) {
|
|
41
42
|
try {
|
|
42
43
|
const result = await audit({ dir: resolvedDir, platform, silent: true });
|
|
43
44
|
platformScores[platform] = result.score;
|
|
45
|
+
if (Array.isArray(result.results)) {
|
|
46
|
+
allAuditResults.push(...result.results);
|
|
47
|
+
}
|
|
44
48
|
} catch {
|
|
45
49
|
platformScores[platform] = 0;
|
|
46
50
|
}
|
|
@@ -55,18 +59,36 @@ async function certifyProject(dir) {
|
|
|
55
59
|
harmonyScore = 0;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
// Determine certification level
|
|
62
|
+
// Determine certification level with security gates
|
|
59
63
|
const scores = Object.values(platformScores);
|
|
60
64
|
const allAbove70 = scores.length > 0 && scores.every(s => s >= 70);
|
|
61
65
|
const allAbove50 = scores.length > 0 && scores.every(s => s >= 50);
|
|
62
66
|
const anyAbove40 = scores.some(s => s >= 40);
|
|
63
67
|
|
|
68
|
+
// Security gate helpers — check whether specific audit checks passed
|
|
69
|
+
const checkPassed = (key) => {
|
|
70
|
+
const match = allAuditResults.find(r => r.key === key);
|
|
71
|
+
return match ? match.passed === true : false;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const gitIgnoreOk = checkPassed('gitIgnoreEnv');
|
|
75
|
+
const secretsOk = checkPassed('secretsProtection');
|
|
76
|
+
const criticalAntiPatterns = allAuditResults.filter(
|
|
77
|
+
r => r.passed === false && r.impact === 'critical'
|
|
78
|
+
);
|
|
79
|
+
const noCriticalAntiPatterns = criticalAntiPatterns.length === 0;
|
|
80
|
+
|
|
81
|
+
// Bronze gate: score >= 40 AND basic security (gitignore + secrets protection)
|
|
82
|
+
const bronzeSecurityGate = gitIgnoreOk && secretsOk;
|
|
83
|
+
// Silver gate: Bronze requirements AND no critical anti-patterns
|
|
84
|
+
const silverSecurityGate = bronzeSecurityGate && noCriticalAntiPatterns;
|
|
85
|
+
|
|
64
86
|
let level;
|
|
65
|
-
if (harmonyScore >= 80 && allAbove70) {
|
|
87
|
+
if (harmonyScore >= 80 && allAbove70 && silverSecurityGate) {
|
|
66
88
|
level = LEVELS.GOLD;
|
|
67
|
-
} else if (harmonyScore >= 60 && allAbove50) {
|
|
89
|
+
} else if (harmonyScore >= 60 && allAbove50 && silverSecurityGate) {
|
|
68
90
|
level = LEVELS.SILVER;
|
|
69
|
-
} else if (anyAbove40) {
|
|
91
|
+
} else if (anyAbove40 && bronzeSecurityGate) {
|
|
70
92
|
level = LEVELS.BRONZE;
|
|
71
93
|
} else {
|
|
72
94
|
level = LEVELS.NONE;
|
|
@@ -80,6 +102,12 @@ async function certifyProject(dir) {
|
|
|
80
102
|
platformScores,
|
|
81
103
|
platforms,
|
|
82
104
|
badge,
|
|
105
|
+
securityGates: {
|
|
106
|
+
gitIgnoreEnv: gitIgnoreOk,
|
|
107
|
+
secretsProtection: secretsOk,
|
|
108
|
+
noCriticalAntiPatterns,
|
|
109
|
+
criticalAntiPatternCount: criticalAntiPatterns.length,
|
|
110
|
+
},
|
|
83
111
|
};
|
|
84
112
|
}
|
|
85
113
|
|
package/src/governance.js
CHANGED
|
@@ -55,7 +55,7 @@ const HOOK_REGISTRY = [
|
|
|
55
55
|
key: 'protect-secrets',
|
|
56
56
|
file: '.claude/hooks/protect-secrets.sh',
|
|
57
57
|
triggerPoint: 'PreToolUse',
|
|
58
|
-
matcher: 'Read|Write|Edit',
|
|
58
|
+
matcher: 'Read|Write|Edit|Bash',
|
|
59
59
|
purpose: 'Blocks direct access to secret or credential files before a tool runs.',
|
|
60
60
|
filesTouched: [],
|
|
61
61
|
sideEffects: ['Stops the action and returns a block decision when a secret path is targeted.'],
|
|
@@ -322,7 +322,7 @@ function buildHookConfig(hookFiles, profileKey) {
|
|
|
322
322
|
const secretsFile = uniqueFiles.find(isSecrets);
|
|
323
323
|
if (secretsFile) {
|
|
324
324
|
hookConfig.PreToolUse = [{
|
|
325
|
-
matcher: 'Read|Write|Edit',
|
|
325
|
+
matcher: 'Read|Write|Edit|Bash',
|
|
326
326
|
hooks: [{
|
|
327
327
|
type: 'command',
|
|
328
328
|
command: hookCommand(secretsFile),
|
package/src/mcp-server.js
CHANGED
package/src/server.js
CHANGED
|
@@ -18,6 +18,10 @@ const SUPPORTED_PLATFORMS = new Set([
|
|
|
18
18
|
'opencode',
|
|
19
19
|
]);
|
|
20
20
|
|
|
21
|
+
function envelope(data) {
|
|
22
|
+
return { data, meta: { version, timestamp: new Date().toISOString() } };
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
function sendJson(res, statusCode, payload) {
|
|
22
26
|
const body = JSON.stringify(payload, null, 2);
|
|
23
27
|
res.writeHead(statusCode, {
|
|
@@ -57,6 +61,11 @@ function createServer(options = {}) {
|
|
|
57
61
|
const baseDir = path.resolve(options.baseDir || process.cwd());
|
|
58
62
|
|
|
59
63
|
return http.createServer(async (req, res) => {
|
|
64
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
65
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
66
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
67
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
68
|
+
|
|
60
69
|
const requestUrl = new URL(req.url || '/', 'http://127.0.0.1');
|
|
61
70
|
|
|
62
71
|
if (req.method !== 'GET') {
|
|
@@ -66,16 +75,16 @@ function createServer(options = {}) {
|
|
|
66
75
|
|
|
67
76
|
try {
|
|
68
77
|
if (requestUrl.pathname === '/api/health') {
|
|
69
|
-
sendJson(res, 200, {
|
|
78
|
+
sendJson(res, 200, envelope({
|
|
70
79
|
status: 'ok',
|
|
71
80
|
version,
|
|
72
81
|
checks: getCatalog().length,
|
|
73
|
-
});
|
|
82
|
+
}));
|
|
74
83
|
return;
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
if (requestUrl.pathname === '/api/catalog') {
|
|
78
|
-
sendJson(res, 200, getCatalog());
|
|
87
|
+
sendJson(res, 200, envelope(getCatalog()));
|
|
79
88
|
return;
|
|
80
89
|
}
|
|
81
90
|
|
|
@@ -83,14 +92,14 @@ function createServer(options = {}) {
|
|
|
83
92
|
const dir = resolveRequestDir(baseDir, requestUrl.searchParams.get('dir'));
|
|
84
93
|
const platform = normalizePlatform(requestUrl.searchParams.get('platform'));
|
|
85
94
|
const result = await audit({ dir, platform, silent: true });
|
|
86
|
-
sendJson(res, 200, result);
|
|
95
|
+
sendJson(res, 200, envelope(result));
|
|
87
96
|
return;
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
if (requestUrl.pathname === '/api/harmony') {
|
|
91
100
|
const dir = resolveRequestDir(baseDir, requestUrl.searchParams.get('dir'));
|
|
92
101
|
const result = await harmonyAudit({ dir, silent: true });
|
|
93
|
-
sendJson(res, 200, result);
|
|
102
|
+
sendJson(res, 200, envelope(result));
|
|
94
103
|
return;
|
|
95
104
|
}
|
|
96
105
|
|
package/src/setup.js
CHANGED
|
@@ -10,6 +10,7 @@ const { ProjectContext } = require('./context');
|
|
|
10
10
|
const { audit } = require('./audit');
|
|
11
11
|
const { buildSettingsForProfile } = require('./governance');
|
|
12
12
|
const { getMcpPackPreflight } = require('./mcp-packs');
|
|
13
|
+
const { writeRollbackArtifact } = require('./activity');
|
|
13
14
|
const { setupCodex } = require('./codex/setup');
|
|
14
15
|
|
|
15
16
|
// ============================================================
|
|
@@ -797,14 +798,21 @@ try {
|
|
|
797
798
|
} catch (e) { /* linter not available or failed - non-blocking */ }
|
|
798
799
|
`,
|
|
799
800
|
'protect-secrets.js': `#!/usr/bin/env node
|
|
800
|
-
// PreToolUse hook - blocks reads of secret files
|
|
801
|
+
// PreToolUse hook - blocks reads of secret files (Read/Write/Edit AND Bash)
|
|
801
802
|
let input = '';
|
|
802
803
|
process.stdin.on('data', d => input += d);
|
|
803
804
|
process.stdin.on('end', () => {
|
|
804
805
|
try {
|
|
805
806
|
const data = JSON.parse(input);
|
|
807
|
+
// Check file_path (for Read/Write/Edit)
|
|
806
808
|
const fp = (data.tool_input && data.tool_input.file_path) || '';
|
|
807
|
-
|
|
809
|
+
// Check command (for Bash)
|
|
810
|
+
const cmd = (data.tool_input && data.tool_input.command) || '';
|
|
811
|
+
|
|
812
|
+
const secretPattern = /\\.env($|\\.)|secrets[\\/\\\\]|credentials|\\.pem$|\\.key$/i;
|
|
813
|
+
const bashSecretPattern = /\\bcat\\s+\\.env|\\bless\\s+\\.env|\\bhead\\s+\\.env|\\btail\\s+\\.env|\\bgrep\\b.*\\.env|\\bcp\\s+\\.env|\\bmv\\s+\\.env|\\bbase64\\s+\\.env|\\bxxd\\s+\\.env|secrets\\/|credentials|\\.pem\\b|\\.key\\b/i;
|
|
814
|
+
|
|
815
|
+
if (secretPattern.test(fp) || bashSecretPattern.test(cmd)) {
|
|
808
816
|
console.log(JSON.stringify({ decision: 'block', reason: 'Blocked: accessing secret/credential files is not allowed.' }));
|
|
809
817
|
} else {
|
|
810
818
|
console.log(JSON.stringify({ decision: 'allow' }));
|
|
@@ -1143,6 +1151,17 @@ async function setup(options) {
|
|
|
1143
1151
|
const mcpPreflightWarnings = getMcpPackPreflight(options.mcpPacks || [])
|
|
1144
1152
|
.filter(item => item.missingEnvVars.length > 0);
|
|
1145
1153
|
|
|
1154
|
+
// Snapshot settings.json before any changes for rollback support
|
|
1155
|
+
const settingsPathForSnapshot = path.join(options.dir, '.claude/settings.json');
|
|
1156
|
+
let settingsSnapshotBefore = null;
|
|
1157
|
+
if (fs.existsSync(settingsPathForSnapshot)) {
|
|
1158
|
+
try {
|
|
1159
|
+
settingsSnapshotBefore = fs.readFileSync(settingsPathForSnapshot, 'utf8');
|
|
1160
|
+
} catch (_) {
|
|
1161
|
+
// Ignore read errors
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1146
1165
|
function log(message = '') {
|
|
1147
1166
|
if (!silent) {
|
|
1148
1167
|
console.log(message);
|
|
@@ -1260,7 +1279,17 @@ async function setup(options) {
|
|
|
1260
1279
|
}
|
|
1261
1280
|
// Merge all fields from newSettings into existing, preserving existing values
|
|
1262
1281
|
if (newSettings.hooks) existingSettings.hooks = newSettings.hooks;
|
|
1263
|
-
if (newSettings.permissions)
|
|
1282
|
+
if (newSettings.permissions) {
|
|
1283
|
+
existingSettings.permissions = existingSettings.permissions || {};
|
|
1284
|
+
// MERGE deny rules: keep existing + add new (deduplicate)
|
|
1285
|
+
const existingDeny = existingSettings.permissions.deny || [];
|
|
1286
|
+
const newDeny = newSettings.permissions.deny || [];
|
|
1287
|
+
existingSettings.permissions.deny = [...new Set([...existingDeny, ...newDeny])];
|
|
1288
|
+
// Only set defaultMode if not already set
|
|
1289
|
+
if (!existingSettings.permissions.defaultMode && newSettings.permissions.defaultMode) {
|
|
1290
|
+
existingSettings.permissions.defaultMode = newSettings.permissions.defaultMode;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1264
1293
|
if (newSettings.mcpServers) existingSettings.mcpServers = { ...existingSettings.mcpServers, ...newSettings.mcpServers };
|
|
1265
1294
|
if (newSettings.nerviqSetup) existingSettings.nerviqSetup = { ...existingSettings.nerviqSetup, ...newSettings.nerviqSetup };
|
|
1266
1295
|
fs.writeFileSync(settingsPath, JSON.stringify(existingSettings, null, 2), 'utf8');
|
|
@@ -1302,6 +1331,29 @@ async function setup(options) {
|
|
|
1302
1331
|
log(' Run \x1b[1mnpx nerviq audit\x1b[0m to check your score.');
|
|
1303
1332
|
log('');
|
|
1304
1333
|
|
|
1334
|
+
// Write rollback artifact so setup can be undone
|
|
1335
|
+
let rollbackId = null;
|
|
1336
|
+
if (writtenFiles.length > 0) {
|
|
1337
|
+
const patchedFiles = [];
|
|
1338
|
+
// If settings.json was modified (not newly created), record the before-snapshot
|
|
1339
|
+
if (settingsSnapshotBefore !== null && writtenFiles.includes('.claude/settings.json')) {
|
|
1340
|
+
patchedFiles.push({
|
|
1341
|
+
file: '.claude/settings.json',
|
|
1342
|
+
before: settingsSnapshotBefore,
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
const rollbackArtifact = writeRollbackArtifact(options.dir, {
|
|
1346
|
+
sourcePlan: 'setup',
|
|
1347
|
+
createdFiles: writtenFiles.filter(f => {
|
|
1348
|
+
// Exclude patched files from createdFiles list
|
|
1349
|
+
return !patchedFiles.some(p => p.file === f);
|
|
1350
|
+
}),
|
|
1351
|
+
patchedFiles,
|
|
1352
|
+
rollbackInstructions: ['Use nerviq rollback to undo this setup'],
|
|
1353
|
+
});
|
|
1354
|
+
rollbackId = rollbackArtifact.id;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1305
1357
|
return {
|
|
1306
1358
|
created,
|
|
1307
1359
|
skipped,
|
|
@@ -1309,6 +1361,7 @@ async function setup(options) {
|
|
|
1309
1361
|
preservedFiles,
|
|
1310
1362
|
stacks,
|
|
1311
1363
|
mcpPreflightWarnings,
|
|
1364
|
+
rollbackId,
|
|
1312
1365
|
};
|
|
1313
1366
|
}
|
|
1314
1367
|
|