@vibecheckai/cli 3.7.0 → 3.9.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/README.md +135 -63
- package/bin/_deprecations.js +447 -19
- package/bin/_router.js +1 -1
- package/bin/registry.js +347 -280
- package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
- package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
- package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
- package/bin/runners/lib/agent-firewall/index.js +200 -0
- package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
- package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
- package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
- package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
- package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +7 -3
- package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
- package/bin/runners/lib/agent-firewall/session/index.js +26 -0
- package/bin/runners/lib/artifact-envelope.js +540 -0
- package/bin/runners/lib/auth-shared.js +977 -0
- package/bin/runners/lib/checkpoint.js +941 -0
- package/bin/runners/lib/cleanup/engine.js +571 -0
- package/bin/runners/lib/cleanup/index.js +53 -0
- package/bin/runners/lib/cleanup/output.js +375 -0
- package/bin/runners/lib/cleanup/rules.js +1060 -0
- package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
- package/bin/runners/lib/doctor/failure-signatures.js +526 -0
- package/bin/runners/lib/doctor/fix-script.js +336 -0
- package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
- package/bin/runners/lib/doctor/modules/index.js +62 -3
- package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
- package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
- package/bin/runners/lib/doctor/safe-repair.js +384 -0
- package/bin/runners/lib/engine/ast-cache.js +210 -210
- package/bin/runners/lib/engine/auth-extractor.js +211 -211
- package/bin/runners/lib/engine/billing-extractor.js +112 -112
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -100
- package/bin/runners/lib/engine/env-extractor.js +207 -207
- package/bin/runners/lib/engine/express-extractor.js +208 -208
- package/bin/runners/lib/engine/extractors.js +849 -849
- package/bin/runners/lib/engine/index.js +207 -207
- package/bin/runners/lib/engine/repo-index.js +514 -514
- package/bin/runners/lib/engine/types.js +124 -124
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/entitlements-v2.js +2 -2
- package/bin/runners/lib/missions/briefing.js +427 -0
- package/bin/runners/lib/missions/checkpoint.js +753 -0
- package/bin/runners/lib/missions/hardening.js +851 -0
- package/bin/runners/lib/missions/plan.js +421 -32
- package/bin/runners/lib/missions/safety-gates.js +645 -0
- package/bin/runners/lib/missions/schema.js +478 -0
- package/bin/runners/lib/packs/bundle.js +675 -0
- package/bin/runners/lib/packs/evidence-pack.js +671 -0
- package/bin/runners/lib/packs/pack-factory.js +837 -0
- package/bin/runners/lib/packs/permissions-pack.js +686 -0
- package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
- package/bin/runners/lib/safelist/index.js +96 -0
- package/bin/runners/lib/safelist/integration.js +334 -0
- package/bin/runners/lib/safelist/matcher.js +696 -0
- package/bin/runners/lib/safelist/schema.js +948 -0
- package/bin/runners/lib/safelist/store.js +438 -0
- package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
- package/bin/runners/lib/ship-gate.js +832 -0
- package/bin/runners/lib/ship-manifest.js +1153 -0
- package/bin/runners/lib/ship-output.js +1 -1
- package/bin/runners/lib/unified-cli-output.js +710 -383
- package/bin/runners/lib/upsell.js +3 -3
- package/bin/runners/lib/why-tree.js +650 -0
- package/bin/runners/runAllowlist.js +33 -4
- package/bin/runners/runApprove.js +240 -1122
- package/bin/runners/runAudit.js +692 -0
- package/bin/runners/runAuth.js +325 -29
- package/bin/runners/runCheckpoint.js +442 -494
- package/bin/runners/runCleanup.js +343 -0
- package/bin/runners/runDoctor.js +269 -19
- package/bin/runners/runFix.js +411 -32
- package/bin/runners/runForge.js +411 -0
- package/bin/runners/runIntent.js +906 -0
- package/bin/runners/runKickoff.js +878 -0
- package/bin/runners/runLaunch.js +2000 -0
- package/bin/runners/runLink.js +785 -0
- package/bin/runners/runMcp.js +1741 -837
- package/bin/runners/runPacks.js +2089 -0
- package/bin/runners/runPolish.js +41 -0
- package/bin/runners/runReality.js +178 -1
- package/bin/runners/runSafelist.js +1190 -0
- package/bin/runners/runScan.js +21 -9
- package/bin/runners/runShield.js +1282 -0
- package/bin/runners/runShip.js +395 -16
- package/bin/vibecheck.js +34 -6
- package/mcp-server/README.md +117 -158
- package/mcp-server/handlers/index.ts +2 -2
- package/mcp-server/handlers/tool-handler.ts +50 -11
- package/mcp-server/index.js +16 -0
- package/mcp-server/intent-firewall-interceptor.js +529 -0
- package/mcp-server/lib/executor.ts +5 -5
- package/mcp-server/lib/index.ts +14 -4
- package/mcp-server/lib/sandbox.test.ts +4 -4
- package/mcp-server/lib/sandbox.ts +2 -2
- package/mcp-server/manifest.json +473 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/registry/tool-registry.js +315 -523
- package/mcp-server/registry/tools.json +442 -428
- package/mcp-server/registry.test.ts +18 -12
- package/mcp-server/tier-auth.js +68 -11
- package/mcp-server/tools-v3.js +70 -16
- package/mcp-server/tsconfig.json +1 -0
- package/package.json +2 -1
- package/bin/runners/runProof.zip +0 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
// bin/runners/lib/missions/safety-gates.js
|
|
2
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
3
|
+
// SAFETY GATES - Pre-flight and post-flight checks for mission safety
|
|
4
|
+
// Gates must pass before missions run, and verify success after
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { RISK_LEVEL, BLAST_RADIUS } = require('./schema');
|
|
11
|
+
const {
|
|
12
|
+
SafetyGateError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
isValidConfidence,
|
|
15
|
+
validateOptions,
|
|
16
|
+
getAuditTrail,
|
|
17
|
+
} = require('./hardening');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gate result structure
|
|
21
|
+
* @typedef {object} GateResult
|
|
22
|
+
* @property {string} gate - Gate name
|
|
23
|
+
* @property {boolean} pass - Whether gate passed
|
|
24
|
+
* @property {string} reason - Human-readable reason
|
|
25
|
+
* @property {string} [remedy] - Suggested fix if gate failed
|
|
26
|
+
* @property {string} [severity] - Gate severity (error, warning, info)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default thresholds for safety gates
|
|
31
|
+
*/
|
|
32
|
+
const DEFAULT_THRESHOLDS = {
|
|
33
|
+
minConfidence: 0.6,
|
|
34
|
+
maxBlastRadius: 10,
|
|
35
|
+
maxFilesPerMission: 6,
|
|
36
|
+
maxLinesChanged: 400,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
40
|
+
// PRE-FLIGHT GATES - Must pass before mission execution
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if mission confidence meets threshold
|
|
45
|
+
* @param {object} mission - Mission object
|
|
46
|
+
* @param {object} options - Gate options
|
|
47
|
+
* @returns {GateResult}
|
|
48
|
+
*/
|
|
49
|
+
function gateConfidence(mission, options = {}) {
|
|
50
|
+
const threshold = options.minConfidence ?? DEFAULT_THRESHOLDS.minConfidence;
|
|
51
|
+
const confidence = mission.safety?.confidence ?? 0.5;
|
|
52
|
+
|
|
53
|
+
const pass = confidence >= threshold;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
gate: 'confidence',
|
|
57
|
+
pass,
|
|
58
|
+
reason: pass
|
|
59
|
+
? `Confidence ${(confidence * 100).toFixed(0)}% >= ${(threshold * 100).toFixed(0)}% threshold`
|
|
60
|
+
: `Confidence ${(confidence * 100).toFixed(0)}% < ${(threshold * 100).toFixed(0)}% threshold`,
|
|
61
|
+
remedy: pass ? null : 'Use --force to override or --min-confidence to adjust threshold',
|
|
62
|
+
severity: pass ? 'info' : 'error',
|
|
63
|
+
value: confidence,
|
|
64
|
+
threshold,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if blast radius is acceptable
|
|
70
|
+
* @param {object} mission - Mission object
|
|
71
|
+
* @param {object} options - Gate options
|
|
72
|
+
* @returns {GateResult}
|
|
73
|
+
*/
|
|
74
|
+
function gateBlastRadius(mission, options = {}) {
|
|
75
|
+
const maxFiles = options.maxBlastRadius ?? DEFAULT_THRESHOLDS.maxBlastRadius;
|
|
76
|
+
const files = mission.scope?.allowedFiles?.length ?? 0;
|
|
77
|
+
const blastRadius = mission.scope?.blastRadius ?? BLAST_RADIUS.MEDIUM;
|
|
78
|
+
|
|
79
|
+
const pass = files <= maxFiles;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
gate: 'blast_radius',
|
|
83
|
+
pass,
|
|
84
|
+
reason: pass
|
|
85
|
+
? `Blast radius: ${files} files (${blastRadius}) <= ${maxFiles} max`
|
|
86
|
+
: `Blast radius: ${files} files (${blastRadius}) > ${maxFiles} max`,
|
|
87
|
+
remedy: pass ? null : 'Use --max-blast to increase limit or --force to override',
|
|
88
|
+
severity: pass ? 'info' : 'error',
|
|
89
|
+
value: files,
|
|
90
|
+
threshold: maxFiles,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if risk level is acceptable for auto-apply
|
|
96
|
+
* @param {object} mission - Mission object
|
|
97
|
+
* @param {object} options - Gate options
|
|
98
|
+
* @returns {GateResult}
|
|
99
|
+
*/
|
|
100
|
+
function gateRiskLevel(mission, options = {}) {
|
|
101
|
+
const riskLevel = mission.safety?.riskLevel ?? RISK_LEVEL.MEDIUM;
|
|
102
|
+
const requiresApproval = mission.safety?.requiresApproval ?? false;
|
|
103
|
+
const forceMode = options.force ?? false;
|
|
104
|
+
|
|
105
|
+
// Critical missions require --force unless explicitly approved
|
|
106
|
+
const isCritical = riskLevel === RISK_LEVEL.CRITICAL;
|
|
107
|
+
const pass = !isCritical || forceMode || !requiresApproval;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
gate: 'risk_level',
|
|
111
|
+
pass,
|
|
112
|
+
reason: pass
|
|
113
|
+
? `Risk level: ${riskLevel.toUpperCase()} ${isCritical ? '(approved with --force)' : ''}`
|
|
114
|
+
: `Risk level: ${riskLevel.toUpperCase()} requires explicit approval`,
|
|
115
|
+
remedy: pass ? null : 'Use --force to approve critical mission execution',
|
|
116
|
+
severity: pass ? (isCritical ? 'warning' : 'info') : 'error',
|
|
117
|
+
value: riskLevel,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check for uncommitted changes in git
|
|
123
|
+
* @param {string} repoRoot - Repository root
|
|
124
|
+
* @param {object} options - Gate options
|
|
125
|
+
* @returns {GateResult}
|
|
126
|
+
*/
|
|
127
|
+
function gateOpenChanges(repoRoot, options = {}) {
|
|
128
|
+
const allowDirty = options.allowDirty ?? false;
|
|
129
|
+
|
|
130
|
+
let isDirty = false;
|
|
131
|
+
let changes = 0;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const status = execSync('git status --porcelain', {
|
|
135
|
+
cwd: repoRoot,
|
|
136
|
+
encoding: 'utf8',
|
|
137
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
138
|
+
}).trim();
|
|
139
|
+
|
|
140
|
+
isDirty = status.length > 0;
|
|
141
|
+
changes = status.split('\n').filter(Boolean).length;
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// Not a git repo or git not available
|
|
144
|
+
return {
|
|
145
|
+
gate: 'open_changes',
|
|
146
|
+
pass: true,
|
|
147
|
+
reason: 'Not a git repository',
|
|
148
|
+
severity: 'info',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const pass = !isDirty || allowDirty;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
gate: 'open_changes',
|
|
156
|
+
pass,
|
|
157
|
+
reason: pass
|
|
158
|
+
? isDirty
|
|
159
|
+
? `${changes} uncommitted changes (allowed)`
|
|
160
|
+
: 'Working directory clean'
|
|
161
|
+
: `${changes} uncommitted changes - commit or stash before fixing`,
|
|
162
|
+
remedy: pass ? null : 'Commit or stash changes before running fix, or use --allow-dirty',
|
|
163
|
+
severity: pass ? 'info' : 'error',
|
|
164
|
+
value: changes,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if target files exist and are readable
|
|
170
|
+
* @param {string} repoRoot - Repository root
|
|
171
|
+
* @param {object} mission - Mission object
|
|
172
|
+
* @returns {GateResult}
|
|
173
|
+
*/
|
|
174
|
+
function gateFilesExist(repoRoot, mission) {
|
|
175
|
+
const allowedFiles = mission.scope?.allowedFiles ?? [];
|
|
176
|
+
const missing = [];
|
|
177
|
+
|
|
178
|
+
for (const file of allowedFiles) {
|
|
179
|
+
const absPath = path.join(repoRoot, file);
|
|
180
|
+
if (!fs.existsSync(absPath)) {
|
|
181
|
+
missing.push(file);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const pass = missing.length === 0;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
gate: 'files_exist',
|
|
189
|
+
pass,
|
|
190
|
+
reason: pass
|
|
191
|
+
? `All ${allowedFiles.length} target files exist`
|
|
192
|
+
: `${missing.length} target files missing: ${missing.slice(0, 3).join(', ')}${missing.length > 3 ? '...' : ''}`,
|
|
193
|
+
remedy: pass ? null : 'Ensure target files exist or re-run scan to update findings',
|
|
194
|
+
severity: pass ? 'info' : 'warning',
|
|
195
|
+
value: allowedFiles.length - missing.length,
|
|
196
|
+
missing,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Run all pre-flight gates
|
|
202
|
+
* @param {string} repoRoot - Repository root
|
|
203
|
+
* @param {object} mission - Mission object
|
|
204
|
+
* @param {object} options - Gate options
|
|
205
|
+
* @returns {object} Combined gate results
|
|
206
|
+
*/
|
|
207
|
+
function runPreFlightGates(repoRoot, mission, options = {}) {
|
|
208
|
+
const audit = getAuditTrail();
|
|
209
|
+
const missionId = mission?.id || 'unknown';
|
|
210
|
+
|
|
211
|
+
audit.debug('preflight_gates_start', { missionId, options });
|
|
212
|
+
|
|
213
|
+
// Validate inputs
|
|
214
|
+
if (!mission || typeof mission !== 'object') {
|
|
215
|
+
audit.error('preflight_gates_invalid_mission', { mission });
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
results: [{
|
|
219
|
+
gate: 'validation',
|
|
220
|
+
pass: false,
|
|
221
|
+
reason: 'Mission object is required',
|
|
222
|
+
severity: 'error',
|
|
223
|
+
}],
|
|
224
|
+
passed: 0,
|
|
225
|
+
failed: 1,
|
|
226
|
+
warnings: 0,
|
|
227
|
+
summary: 'Mission validation failed',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Sanitize options with defaults
|
|
232
|
+
const safeOptions = {
|
|
233
|
+
minConfidence: options.minConfidence ?? DEFAULT_THRESHOLDS.minConfidence,
|
|
234
|
+
maxBlastRadius: options.maxBlastRadius ?? DEFAULT_THRESHOLDS.maxBlastRadius,
|
|
235
|
+
force: options.force ?? false,
|
|
236
|
+
allowDirty: options.allowDirty ?? false,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Run all gates
|
|
240
|
+
const results = [];
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
results.push(gateConfidence(mission, safeOptions));
|
|
244
|
+
} catch (e) {
|
|
245
|
+
results.push({ gate: 'confidence', pass: false, reason: `Gate error: ${e.message}`, severity: 'error' });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
results.push(gateBlastRadius(mission, safeOptions));
|
|
250
|
+
} catch (e) {
|
|
251
|
+
results.push({ gate: 'blast_radius', pass: false, reason: `Gate error: ${e.message}`, severity: 'error' });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
results.push(gateRiskLevel(mission, safeOptions));
|
|
256
|
+
} catch (e) {
|
|
257
|
+
results.push({ gate: 'risk_level', pass: false, reason: `Gate error: ${e.message}`, severity: 'error' });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
results.push(gateOpenChanges(repoRoot, safeOptions));
|
|
262
|
+
} catch (e) {
|
|
263
|
+
results.push({ gate: 'open_changes', pass: false, reason: `Gate error: ${e.message}`, severity: 'error' });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
results.push(gateFilesExist(repoRoot, mission));
|
|
268
|
+
} catch (e) {
|
|
269
|
+
results.push({ gate: 'files_exist', pass: false, reason: `Gate error: ${e.message}`, severity: 'error' });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const passed = results.filter(r => r.pass);
|
|
273
|
+
const failed = results.filter(r => !r.pass);
|
|
274
|
+
const warnings = results.filter(r => r.severity === 'warning');
|
|
275
|
+
|
|
276
|
+
const gateResults = {
|
|
277
|
+
ok: failed.length === 0,
|
|
278
|
+
results,
|
|
279
|
+
passed: passed.length,
|
|
280
|
+
failed: failed.length,
|
|
281
|
+
warnings: warnings.length,
|
|
282
|
+
summary: failed.length === 0
|
|
283
|
+
? `All ${results.length} pre-flight gates passed`
|
|
284
|
+
: `${failed.length} gate(s) failed: ${failed.map(f => f.gate).join(', ')}`,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
audit.info('preflight_gates_complete', {
|
|
288
|
+
missionId,
|
|
289
|
+
ok: gateResults.ok,
|
|
290
|
+
passed: passed.length,
|
|
291
|
+
failed: failed.length,
|
|
292
|
+
failedGates: failed.map(f => f.gate),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return gateResults;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
299
|
+
// POST-FLIGHT GATES - Verify success after mission execution
|
|
300
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Check if findings were reduced
|
|
304
|
+
* @param {object} before - Ship results before fix
|
|
305
|
+
* @param {object} after - Ship results after fix
|
|
306
|
+
* @param {object} mission - Mission object
|
|
307
|
+
* @returns {GateResult}
|
|
308
|
+
*/
|
|
309
|
+
function gateFindingsReduced(before, after, mission) {
|
|
310
|
+
const beforeFindings = before?.report?.findings ?? before?.findings ?? [];
|
|
311
|
+
const afterFindings = after?.report?.findings ?? after?.findings ?? [];
|
|
312
|
+
|
|
313
|
+
const targetIds = new Set(mission.objective?.targetFindingIds ?? []);
|
|
314
|
+
|
|
315
|
+
// Count target findings before and after
|
|
316
|
+
const targetsBefore = beforeFindings.filter(f => targetIds.has(f.id)).length;
|
|
317
|
+
const targetsAfter = afterFindings.filter(f => targetIds.has(f.id)).length;
|
|
318
|
+
|
|
319
|
+
const reduced = targetsAfter < targetsBefore;
|
|
320
|
+
const eliminated = targetsAfter === 0;
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
gate: 'findings_reduced',
|
|
324
|
+
pass: reduced,
|
|
325
|
+
reason: reduced
|
|
326
|
+
? eliminated
|
|
327
|
+
? `All ${targetsBefore} target findings eliminated`
|
|
328
|
+
: `Target findings reduced: ${targetsBefore} → ${targetsAfter}`
|
|
329
|
+
: `Target findings not reduced: ${targetsBefore} → ${targetsAfter}`,
|
|
330
|
+
remedy: reduced ? null : 'Fix did not address target findings - rollback recommended',
|
|
331
|
+
severity: reduced ? 'info' : 'error',
|
|
332
|
+
value: { before: targetsBefore, after: targetsAfter },
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Check for regressions (new findings introduced)
|
|
338
|
+
* @param {object} before - Ship results before fix
|
|
339
|
+
* @param {object} after - Ship results after fix
|
|
340
|
+
* @returns {GateResult}
|
|
341
|
+
*/
|
|
342
|
+
function gateNoRegressions(before, after) {
|
|
343
|
+
const beforeFindings = before?.report?.findings ?? before?.findings ?? [];
|
|
344
|
+
const afterFindings = after?.report?.findings ?? after?.findings ?? [];
|
|
345
|
+
|
|
346
|
+
const beforeIds = new Set(beforeFindings.map(f => f.id));
|
|
347
|
+
const newFindings = afterFindings.filter(f => !beforeIds.has(f.id));
|
|
348
|
+
|
|
349
|
+
// Only count new BLOCK findings as regressions
|
|
350
|
+
const newBlocks = newFindings.filter(f => f.severity === 'BLOCK');
|
|
351
|
+
|
|
352
|
+
const pass = newBlocks.length === 0;
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
gate: 'no_regressions',
|
|
356
|
+
pass,
|
|
357
|
+
reason: pass
|
|
358
|
+
? newFindings.length === 0
|
|
359
|
+
? 'No new findings introduced'
|
|
360
|
+
: `${newFindings.length} new non-blocking findings (acceptable)`
|
|
361
|
+
: `${newBlocks.length} new BLOCK findings introduced`,
|
|
362
|
+
remedy: pass ? null : 'Fix introduced new blocking issues - rollback recommended',
|
|
363
|
+
severity: pass ? 'info' : 'error',
|
|
364
|
+
value: { newTotal: newFindings.length, newBlocks: newBlocks.length },
|
|
365
|
+
newFindings: newBlocks,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Check if overall score decreased
|
|
371
|
+
* @param {object} before - Ship results before fix
|
|
372
|
+
* @param {object} after - Ship results after fix
|
|
373
|
+
* @returns {GateResult}
|
|
374
|
+
*/
|
|
375
|
+
function gateScoreDecrease(before, after) {
|
|
376
|
+
const beforeFindings = before?.report?.findings ?? before?.findings ?? [];
|
|
377
|
+
const afterFindings = after?.report?.findings ?? after?.findings ?? [];
|
|
378
|
+
|
|
379
|
+
// Calculate scores
|
|
380
|
+
const score = (findings) => {
|
|
381
|
+
let s = 0;
|
|
382
|
+
for (const f of findings) {
|
|
383
|
+
if (f.severity === 'BLOCK') s += 10;
|
|
384
|
+
else if (f.severity === 'WARN') s += 3;
|
|
385
|
+
else s += 1;
|
|
386
|
+
}
|
|
387
|
+
return s;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const beforeScore = score(beforeFindings);
|
|
391
|
+
const afterScore = score(afterFindings);
|
|
392
|
+
|
|
393
|
+
const decreased = afterScore < beforeScore;
|
|
394
|
+
const delta = beforeScore - afterScore;
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
gate: 'score_decrease',
|
|
398
|
+
pass: decreased,
|
|
399
|
+
reason: decreased
|
|
400
|
+
? `Score decreased: ${beforeScore} → ${afterScore} (${delta > 0 ? '-' : '+'}${Math.abs(delta)})`
|
|
401
|
+
: `Score not decreased: ${beforeScore} → ${afterScore}`,
|
|
402
|
+
remedy: decreased ? null : 'Fix did not improve overall score',
|
|
403
|
+
severity: decreased ? 'info' : 'warning',
|
|
404
|
+
value: { before: beforeScore, after: afterScore, delta },
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Check if tests pass (optional - requires test runner config)
|
|
410
|
+
* @param {string} repoRoot - Repository root
|
|
411
|
+
* @param {object} options - Test options
|
|
412
|
+
* @returns {GateResult}
|
|
413
|
+
*/
|
|
414
|
+
function gateTestsPass(repoRoot, options = {}) {
|
|
415
|
+
const testCommand = options.testCommand ?? null;
|
|
416
|
+
const skipTests = options.skipTests ?? true;
|
|
417
|
+
|
|
418
|
+
if (skipTests || !testCommand) {
|
|
419
|
+
return {
|
|
420
|
+
gate: 'tests_pass',
|
|
421
|
+
pass: true,
|
|
422
|
+
reason: 'Tests skipped (not configured)',
|
|
423
|
+
severity: 'info',
|
|
424
|
+
skipped: true,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
execSync(testCommand, {
|
|
430
|
+
cwd: repoRoot,
|
|
431
|
+
encoding: 'utf8',
|
|
432
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
433
|
+
timeout: options.testTimeout ?? 60000,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
gate: 'tests_pass',
|
|
438
|
+
pass: true,
|
|
439
|
+
reason: 'All tests passed',
|
|
440
|
+
severity: 'info',
|
|
441
|
+
};
|
|
442
|
+
} catch (e) {
|
|
443
|
+
return {
|
|
444
|
+
gate: 'tests_pass',
|
|
445
|
+
pass: false,
|
|
446
|
+
reason: `Tests failed: ${e.message?.slice(0, 100) ?? 'Unknown error'}`,
|
|
447
|
+
remedy: 'Fix test failures or use --skip-tests to bypass',
|
|
448
|
+
severity: 'error',
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Run all post-flight gates
|
|
455
|
+
* @param {string} repoRoot - Repository root
|
|
456
|
+
* @param {object} mission - Mission object
|
|
457
|
+
* @param {object} before - Ship results before fix
|
|
458
|
+
* @param {object} after - Ship results after fix
|
|
459
|
+
* @param {object} options - Gate options
|
|
460
|
+
* @returns {object} Combined gate results
|
|
461
|
+
*/
|
|
462
|
+
function runPostFlightGates(repoRoot, mission, before, after, options = {}) {
|
|
463
|
+
const audit = getAuditTrail();
|
|
464
|
+
const missionId = mission?.id || 'unknown';
|
|
465
|
+
|
|
466
|
+
audit.debug('postflight_gates_start', { missionId });
|
|
467
|
+
|
|
468
|
+
// Validate inputs
|
|
469
|
+
if (!before || !after) {
|
|
470
|
+
audit.error('postflight_gates_missing_results', { missionId, hasBefore: !!before, hasAfter: !!after });
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
results: [{
|
|
474
|
+
gate: 'validation',
|
|
475
|
+
pass: false,
|
|
476
|
+
reason: 'Before and after ship results are required',
|
|
477
|
+
severity: 'error',
|
|
478
|
+
}],
|
|
479
|
+
passed: 0,
|
|
480
|
+
failed: 1,
|
|
481
|
+
warnings: 0,
|
|
482
|
+
shouldRollback: true,
|
|
483
|
+
summary: 'Post-flight validation failed',
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const results = [];
|
|
488
|
+
|
|
489
|
+
// Run gates with error handling
|
|
490
|
+
try {
|
|
491
|
+
results.push(gateFindingsReduced(before, after, mission));
|
|
492
|
+
} catch (e) {
|
|
493
|
+
results.push({ gate: 'findings_reduced', pass: false, reason: `Gate error: ${e.message}`, severity: 'error' });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
results.push(gateNoRegressions(before, after));
|
|
498
|
+
} catch (e) {
|
|
499
|
+
results.push({ gate: 'no_regressions', pass: false, reason: `Gate error: ${e.message}`, severity: 'error' });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
results.push(gateScoreDecrease(before, after));
|
|
504
|
+
} catch (e) {
|
|
505
|
+
results.push({ gate: 'score_decrease', pass: false, reason: `Gate error: ${e.message}`, severity: 'error' });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Optionally run tests
|
|
509
|
+
if (options.runTests) {
|
|
510
|
+
try {
|
|
511
|
+
results.push(gateTestsPass(repoRoot, options));
|
|
512
|
+
} catch (e) {
|
|
513
|
+
results.push({ gate: 'tests_pass', pass: false, reason: `Gate error: ${e.message}`, severity: 'error' });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const passed = results.filter(r => r.pass);
|
|
518
|
+
const failed = results.filter(r => !r.pass);
|
|
519
|
+
const warnings = results.filter(r => r.severity === 'warning');
|
|
520
|
+
|
|
521
|
+
// Determine if rollback is recommended
|
|
522
|
+
const shouldRollback = failed.some(f =>
|
|
523
|
+
f.gate === 'findings_reduced' ||
|
|
524
|
+
f.gate === 'no_regressions' ||
|
|
525
|
+
f.gate === 'tests_pass'
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const gateResults = {
|
|
529
|
+
ok: failed.length === 0,
|
|
530
|
+
results,
|
|
531
|
+
passed: passed.length,
|
|
532
|
+
failed: failed.length,
|
|
533
|
+
warnings: warnings.length,
|
|
534
|
+
shouldRollback,
|
|
535
|
+
summary: failed.length === 0
|
|
536
|
+
? `All ${results.length} post-flight gates passed`
|
|
537
|
+
: `${failed.length} gate(s) failed: ${failed.map(f => f.gate).join(', ')}`,
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
audit.info('postflight_gates_complete', {
|
|
541
|
+
missionId,
|
|
542
|
+
ok: gateResults.ok,
|
|
543
|
+
passed: passed.length,
|
|
544
|
+
failed: failed.length,
|
|
545
|
+
shouldRollback,
|
|
546
|
+
failedGates: failed.map(f => f.gate),
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
return gateResults;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
553
|
+
// COMBINED GATE RUNNER
|
|
554
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Format gate results for display
|
|
558
|
+
* @param {object[]} results - Array of gate results
|
|
559
|
+
* @returns {string[]} Formatted lines
|
|
560
|
+
*/
|
|
561
|
+
function formatGateResults(results) {
|
|
562
|
+
const lines = [];
|
|
563
|
+
|
|
564
|
+
for (const r of results) {
|
|
565
|
+
const icon = r.pass
|
|
566
|
+
? (r.severity === 'warning' ? '⚠' : '✓')
|
|
567
|
+
: '✗';
|
|
568
|
+
const color = r.pass
|
|
569
|
+
? (r.severity === 'warning' ? 'yellow' : 'green')
|
|
570
|
+
: 'red';
|
|
571
|
+
|
|
572
|
+
lines.push({
|
|
573
|
+
icon,
|
|
574
|
+
color,
|
|
575
|
+
gate: r.gate,
|
|
576
|
+
reason: r.reason,
|
|
577
|
+
remedy: r.remedy,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return lines;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Check if mission should auto-apply based on gates
|
|
586
|
+
* @param {string} repoRoot - Repository root
|
|
587
|
+
* @param {object} mission - Mission object
|
|
588
|
+
* @param {object} options - Gate options
|
|
589
|
+
* @returns {object} Decision result
|
|
590
|
+
*/
|
|
591
|
+
function shouldAutoApply(repoRoot, mission, options = {}) {
|
|
592
|
+
const preFlightResults = runPreFlightGates(repoRoot, mission, options);
|
|
593
|
+
|
|
594
|
+
if (!preFlightResults.ok) {
|
|
595
|
+
return {
|
|
596
|
+
shouldApply: false,
|
|
597
|
+
reason: 'Pre-flight gates failed',
|
|
598
|
+
preFlightResults,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Check mission-level safety
|
|
603
|
+
const isSafe =
|
|
604
|
+
mission.safety?.reversible !== false &&
|
|
605
|
+
mission.safety?.riskLevel !== RISK_LEVEL.CRITICAL &&
|
|
606
|
+
mission.safety?.confidence >= (options.minConfidence ?? DEFAULT_THRESHOLDS.minConfidence);
|
|
607
|
+
|
|
608
|
+
if (!isSafe && !options.force) {
|
|
609
|
+
return {
|
|
610
|
+
shouldApply: false,
|
|
611
|
+
reason: 'Mission does not meet auto-apply criteria',
|
|
612
|
+
preFlightResults,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
shouldApply: true,
|
|
618
|
+
reason: 'All safety checks passed',
|
|
619
|
+
preFlightResults,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
module.exports = {
|
|
624
|
+
// Pre-flight gates
|
|
625
|
+
gateConfidence,
|
|
626
|
+
gateBlastRadius,
|
|
627
|
+
gateRiskLevel,
|
|
628
|
+
gateOpenChanges,
|
|
629
|
+
gateFilesExist,
|
|
630
|
+
runPreFlightGates,
|
|
631
|
+
|
|
632
|
+
// Post-flight gates
|
|
633
|
+
gateFindingsReduced,
|
|
634
|
+
gateNoRegressions,
|
|
635
|
+
gateScoreDecrease,
|
|
636
|
+
gateTestsPass,
|
|
637
|
+
runPostFlightGates,
|
|
638
|
+
|
|
639
|
+
// Utilities
|
|
640
|
+
formatGateResults,
|
|
641
|
+
shouldAutoApply,
|
|
642
|
+
|
|
643
|
+
// Constants
|
|
644
|
+
DEFAULT_THRESHOLDS,
|
|
645
|
+
};
|