@saiteja1123/mcp-server 1.1.4 → 1.1.6

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.
Files changed (37) hide show
  1. package/package.json +59 -55
  2. package/src/api-scan.mjs +362 -93
  3. package/src/cli.js +771 -322
  4. package/src/deep-scan/contracts.js +201 -0
  5. package/src/deep-scan/deterministic-scan.js +337 -0
  6. package/src/deep-scan/index.js +109 -0
  7. package/src/deep-scan/project-map.js +507 -0
  8. package/src/deep-scan/ralph-accept.js +510 -0
  9. package/src/deep-scan/ralph-compare.js +498 -0
  10. package/src/deep-scan/ralph-tasks.js +598 -0
  11. package/src/deep-scan/ralph-track.js +548 -0
  12. package/src/deep-scan/registry.js +159 -0
  13. package/src/deep-scan/runtime.js +275 -0
  14. package/src/deep-scan/sample-steppers.js +128 -0
  15. package/src/deep-scan/sourceSafe.js +73 -0
  16. package/src/deep-scan/status.js +70 -0
  17. package/src/deep-scan/store.js +57 -0
  18. package/src/deep-scan/test-plan.js +760 -0
  19. package/src/index.js +6 -5
  20. package/src/lock.mjs +55 -14
  21. package/src/mcp-config.mjs +161 -0
  22. package/src/middleware/governance.js +135 -0
  23. package/src/orchestrator/runScan.js +211 -0
  24. package/src/project-bindings.mjs +215 -0
  25. package/src/rule-engine/index.js +2 -1
  26. package/src/rule-engine/localScan.js +39 -12
  27. package/src/rule-engine/metadata.js +20 -0
  28. package/src/rule-engine/prompt.js +6 -5
  29. package/src/rule-engine/rules.js +71 -43
  30. package/src/rule-engine/score.js +5 -4
  31. package/src/security/pathGuard.js +170 -0
  32. package/src/selftest.js +2473 -0
  33. package/src/server.js +109 -150
  34. package/src/tools/deepScan.js +286 -0
  35. package/src/tools/localScan.js +85 -0
  36. package/src/tools/projects.js +124 -0
  37. package/src/tools/scanFile.js +131 -0
@@ -0,0 +1,548 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import { createArtifactRef, validateRunIdSegment } from './contracts.js';
5
+ import { assertSourceSafePayload, cloneSourceSafe } from './sourceSafe.js';
6
+ import { validateRalphComparisonArtifact } from './ralph-compare.js';
7
+
8
+ export const RALPH_FAILURE_STATE_SCHEMA_VERSION = 'ralph_failure_state.v1';
9
+ export const RALPH_FAILURE_STATE_STEPPER_ID = 'ralph.track';
10
+ export const RALPH_FAILURE_STATE_STEPPER_VERSION = '1.0.0';
11
+ export const RALPH_ESCALATION_STATUSES = Object.freeze([
12
+ 'normal',
13
+ 'escalated',
14
+ 'manualReview',
15
+ 'acceptedRisk',
16
+ ]);
17
+
18
+ export const DEFAULT_RALPH_LOOP_RETRY = Object.freeze({
19
+ maxAttempts: 3,
20
+ escalateAt: 2,
21
+ trackSeverities: ['critical', 'high'],
22
+ });
23
+
24
+ const FAILURE_STATE_FILE = 'ralph-failure-state.json';
25
+ const SHA256 = /^[a-f0-9]{64}$/i;
26
+ const UUIDISH = /^[a-zA-Z0-9_.:-]+$/;
27
+ const RISKY_SEVERITY = new Set(['critical', 'high']);
28
+ const nowIso = () => new Date().toISOString();
29
+
30
+ function stableSort(value) {
31
+ if (Array.isArray(value)) return value.map(stableSort);
32
+ if (!value || typeof value !== 'object') return value;
33
+ return Object.keys(value)
34
+ .sort()
35
+ .reduce((acc, key) => {
36
+ acc[key] = stableSort(value[key]);
37
+ return acc;
38
+ }, {});
39
+ }
40
+
41
+ function stableStringify(value) {
42
+ return JSON.stringify(stableSort(value));
43
+ }
44
+
45
+ function sha256(value) {
46
+ return crypto.createHash('sha256').update(value).digest('hex');
47
+ }
48
+
49
+ function requireString(value, name, { max = 500, pattern = null } = {}) {
50
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} must be a non-empty string`);
51
+ if (value.length > max) throw new Error(`${name} must be ${max} characters or fewer`);
52
+ if (pattern && !pattern.test(value)) throw new Error(`${name} has an invalid format`);
53
+ return value;
54
+ }
55
+
56
+ function optionalString(value, name, options) {
57
+ if (value === undefined || value === null) return null;
58
+ return requireString(value, name, options);
59
+ }
60
+
61
+ function requireIsoDate(value, name) {
62
+ requireString(value, name, { max: 40 });
63
+ if (Number.isNaN(Date.parse(value))) throw new Error(`${name} must be an ISO timestamp`);
64
+ return value;
65
+ }
66
+
67
+ function asArray(value, name) {
68
+ if (value === undefined) return [];
69
+ if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
70
+ return value;
71
+ }
72
+
73
+ function normalizeSeverity(value) {
74
+ const severity = String(value || 'medium').toLowerCase();
75
+ return RISKY_SEVERITY.has(severity) || severity === 'medium' || severity === 'low' ? severity : 'medium';
76
+ }
77
+
78
+ function normalizeRetryConfig(input = {}) {
79
+ const trackSeverities = asArray(input.trackSeverities, 'ralphLoop.retry.trackSeverities')
80
+ .map((item) => normalizeSeverity(item))
81
+ .filter((item, index, all) => all.indexOf(item) === index);
82
+ const maxAttempts = Number.isInteger(Number(input.maxAttempts)) ? Number(input.maxAttempts) : DEFAULT_RALPH_LOOP_RETRY.maxAttempts;
83
+ const escalateAt = Number.isInteger(Number(input.escalateAt)) ? Number(input.escalateAt) : DEFAULT_RALPH_LOOP_RETRY.escalateAt;
84
+ if (maxAttempts < 1) throw new Error('ralphLoop.retry.maxAttempts must be at least 1');
85
+ if (escalateAt < 1) throw new Error('ralphLoop.retry.escalateAt must be at least 1');
86
+ if (escalateAt > maxAttempts) throw new Error('ralphLoop.retry.escalateAt must not exceed maxAttempts');
87
+ return {
88
+ maxAttempts,
89
+ escalateAt,
90
+ trackSeverities: trackSeverities.length > 0 ? trackSeverities : [...DEFAULT_RALPH_LOOP_RETRY.trackSeverities],
91
+ };
92
+ }
93
+
94
+ function taskIdByFindingId(agentFixTasks = {}) {
95
+ const map = new Map();
96
+ for (const task of asArray(agentFixTasks.tasks, 'agentFixTasks.tasks')) {
97
+ for (const findingId of asArray(task.findingIds, 'agentFixTask.findingIds')) {
98
+ if (!map.has(findingId)) map.set(findingId, task.taskId || null);
99
+ }
100
+ }
101
+ return map;
102
+ }
103
+
104
+ function previousFailureMap(previousFailureState = {}) {
105
+ return new Map(
106
+ asArray(previousFailureState.failures, 'ralphFailureState.failures')
107
+ .map((record) => [record.findingKey, record]),
108
+ );
109
+ }
110
+
111
+ function shouldTrackSeverity(severity, retryConfig) {
112
+ return retryConfig.trackSeverities.includes(normalizeSeverity(severity));
113
+ }
114
+
115
+ function failureReasonFor(status, severity) {
116
+ return `${status}_${normalizeSeverity(severity)}`;
117
+ }
118
+
119
+ function nextActionFor({ escalationStatus, attemptCount, retryConfig, comparisonStatus }) {
120
+ if (escalationStatus === 'manualReview') {
121
+ return 'Manual review required before treating this finding as resolved or accepted.';
122
+ }
123
+ if (escalationStatus === 'acceptedRisk') {
124
+ return 'Accepted-risk record is preserved; do not claim the finding is fixed without explicit approval evidence.';
125
+ }
126
+ if (escalationStatus === 'escalated' || attemptCount >= retryConfig.maxAttempts) {
127
+ return 'Repeated failure threshold reached; require human review or accepted-risk approval before release reliance.';
128
+ }
129
+ if (comparisonStatus === 'new') {
130
+ return 'New high-risk finding detected; apply the smallest safe local fix and rerun Vibesecur verification.';
131
+ }
132
+ return 'Apply a narrower local fix, rerun relevant tests, and compare again through MCP Deep Scan.';
133
+ }
134
+
135
+ function escalationStatusFor({ comparisonStatus, attemptCount, retryConfig, lowConfidence }) {
136
+ if (comparisonStatus === 'manualReview' || lowConfidence) return 'manualReview';
137
+ if (comparisonStatus === 'acceptedRisk') return 'acceptedRisk';
138
+ if (attemptCount >= retryConfig.maxAttempts) return 'escalated';
139
+ if (attemptCount >= retryConfig.escalateAt) return 'escalated';
140
+ return 'normal';
141
+ }
142
+
143
+ export function buildRalphFailureStateArtifact({
144
+ comparisonArtifact,
145
+ previousFailureState = {},
146
+ agentFixTasks = {},
147
+ retryConfig = DEFAULT_RALPH_LOOP_RETRY,
148
+ generatedAt = nowIso(),
149
+ sourceRefs = {},
150
+ } = {}) {
151
+ const comparison = validateRalphComparisonArtifact(comparisonArtifact);
152
+ const previous = cloneSourceSafe(previousFailureState || {}, 'ralphFailureState.previous');
153
+ const tasks = cloneSourceSafe(agentFixTasks || {}, 'ralphFailureState.agentFixTasks');
154
+ const retry = normalizeRetryConfig(retryConfig);
155
+ const priorByKey = previousFailureMap(previous);
156
+ const taskIds = taskIdByFindingId(tasks);
157
+ const failures = [];
158
+ let resolvedCount = 0;
159
+
160
+ for (const row of comparison.comparisons) {
161
+ const severity = normalizeSeverity(row.current?.severity || row.previous?.severity || 'medium');
162
+ const findingId = row.current?.id || row.previous?.id || null;
163
+ const prior = priorByKey.get(row.findingKey) || null;
164
+
165
+ if (row.status === 'resolved' || row.status === 'improved') {
166
+ if (prior) resolvedCount += 1;
167
+ continue;
168
+ }
169
+
170
+ if (row.status === 'manualReview') {
171
+ failures.push({
172
+ findingKey: row.findingKey,
173
+ findingId,
174
+ taskId: findingId ? (taskIds.get(findingId) || prior?.taskId || null) : (prior?.taskId || null),
175
+ severity,
176
+ attemptCount: prior?.attemptCount || 0,
177
+ lastFailureReason: row.manualReviewReason || 'manual_review_required',
178
+ lastSeenAt: generatedAt,
179
+ escalationStatus: 'manualReview',
180
+ comparisonStatus: row.status,
181
+ nextAction: nextActionFor({
182
+ escalationStatus: 'manualReview',
183
+ attemptCount: prior?.attemptCount || 0,
184
+ retryConfig: retry,
185
+ comparisonStatus: row.status,
186
+ }),
187
+ });
188
+ continue;
189
+ }
190
+
191
+ if (row.status === 'acceptedRisk') {
192
+ failures.push({
193
+ findingKey: row.findingKey,
194
+ findingId,
195
+ taskId: findingId ? (taskIds.get(findingId) || prior?.taskId || null) : (prior?.taskId || null),
196
+ severity,
197
+ attemptCount: prior?.attemptCount || 0,
198
+ lastFailureReason: 'accepted_risk_recorded',
199
+ lastSeenAt: generatedAt,
200
+ escalationStatus: 'acceptedRisk',
201
+ comparisonStatus: row.status,
202
+ acceptedRiskRef: row.acceptedRiskRef || null,
203
+ nextAction: nextActionFor({
204
+ escalationStatus: 'acceptedRisk',
205
+ attemptCount: prior?.attemptCount || 0,
206
+ retryConfig: retry,
207
+ comparisonStatus: row.status,
208
+ }),
209
+ });
210
+ continue;
211
+ }
212
+
213
+ const increments = row.status === 'unchanged' || row.status === 'worsened';
214
+ const tracked = shouldTrackSeverity(severity, retry);
215
+ if (!tracked && row.status !== 'new') continue;
216
+
217
+ let attemptCount = prior?.attemptCount || 0;
218
+ if (row.status === 'new' && tracked) {
219
+ attemptCount = 1;
220
+ } else if (increments && tracked) {
221
+ attemptCount += 1;
222
+ } else if (row.status === 'new') {
223
+ continue;
224
+ } else if (!increments) {
225
+ continue;
226
+ }
227
+
228
+ const escalationStatus = escalationStatusFor({
229
+ comparisonStatus: row.status,
230
+ attemptCount,
231
+ retryConfig: retry,
232
+ lowConfidence: row.matchConfidence === 'low',
233
+ });
234
+ failures.push({
235
+ findingKey: row.findingKey,
236
+ findingId,
237
+ taskId: findingId ? (taskIds.get(findingId) || prior?.taskId || null) : (prior?.taskId || null),
238
+ severity,
239
+ attemptCount,
240
+ lastFailureReason: failureReasonFor(row.status, severity),
241
+ lastSeenAt: generatedAt,
242
+ escalationStatus,
243
+ comparisonStatus: row.status,
244
+ nextAction: nextActionFor({
245
+ escalationStatus,
246
+ attemptCount,
247
+ retryConfig: retry,
248
+ comparisonStatus: row.status,
249
+ }),
250
+ });
251
+ }
252
+
253
+ failures.sort((a, b) => b.attemptCount - a.attemptCount || a.findingKey.localeCompare(b.findingKey));
254
+
255
+ const artifact = {
256
+ schemaVersion: RALPH_FAILURE_STATE_SCHEMA_VERSION,
257
+ generatedAt: requireIsoDate(generatedAt, 'ralphFailureState.generatedAt'),
258
+ root: cloneSourceSafe(comparison.root || {}, 'ralphFailureState.root'),
259
+ sources: {
260
+ ralphComparison: sourceRefs.ralphComparison || 'ralph_comparison',
261
+ previousFailureState: sourceRefs.previousFailureState || null,
262
+ agentFixTasks: sourceRefs.agentFixTasks || null,
263
+ },
264
+ retryConfig: retry,
265
+ summary: {
266
+ activeFailureCount: failures.length,
267
+ repeatedFailureCount: failures.filter((item) => item.attemptCount > 1).length,
268
+ escalatedCount: failures.filter((item) => item.escalationStatus === 'escalated').length,
269
+ manualReviewCount: failures.filter((item) => item.escalationStatus === 'manualReview').length,
270
+ acceptedRiskCount: failures.filter((item) => item.escalationStatus === 'acceptedRisk').length,
271
+ resolvedCount,
272
+ topRepeatedFailures: failures
273
+ .filter((item) => item.attemptCount > 1)
274
+ .slice(0, 5)
275
+ .map((item) => ({
276
+ findingKey: item.findingKey,
277
+ findingId: item.findingId,
278
+ attemptCount: item.attemptCount,
279
+ severity: item.severity,
280
+ escalationStatus: item.escalationStatus,
281
+ })),
282
+ },
283
+ reportHandoff: {
284
+ repeatedFailureRiskVisible: failures.some((item) => item.attemptCount > 1 || item.escalationStatus === 'escalated'),
285
+ requiresHumanReview: failures.some((item) =>
286
+ item.escalationStatus === 'manualReview' || item.attemptCount >= retry.maxAttempts),
287
+ topRepeatedFailures: failures
288
+ .filter((item) => item.attemptCount > 1)
289
+ .slice(0, 5),
290
+ note: 'Repeated failure tracking uses comparison metadata only; no raw source or remediation diffs are stored.',
291
+ },
292
+ failures,
293
+ privacy: {
294
+ rawSourceStored: false,
295
+ secretValuesCaptured: false,
296
+ remediationDiffsStored: false,
297
+ artifactStorage: 'local_metadata_and_hash_refs',
298
+ },
299
+ };
300
+
301
+ return validateRalphFailureStateArtifact(artifact);
302
+ }
303
+
304
+ function validateFailureRecord(input = {}) {
305
+ assertSourceSafePayload(input, 'ralphFailureState.record');
306
+ const escalationStatus = requireString(input.escalationStatus, 'ralphFailureState.escalationStatus', { max: 40 });
307
+ if (!RALPH_ESCALATION_STATUSES.includes(escalationStatus)) {
308
+ throw new Error(`ralphFailureState.escalationStatus must be one of ${RALPH_ESCALATION_STATUSES.join(', ')}`);
309
+ }
310
+ const record = {
311
+ findingKey: requireString(input.findingKey, 'ralphFailureState.findingKey', { max: 64, pattern: SHA256 }),
312
+ findingId: optionalString(input.findingId, 'ralphFailureState.findingId', { max: 120, pattern: UUIDISH }),
313
+ taskId: optionalString(input.taskId, 'ralphFailureState.taskId', { max: 120, pattern: UUIDISH }),
314
+ severity: normalizeSeverity(input.severity),
315
+ attemptCount: Number.isInteger(input.attemptCount) ? input.attemptCount : 0,
316
+ lastFailureReason: requireString(input.lastFailureReason, 'ralphFailureState.lastFailureReason', { max: 500 }),
317
+ lastSeenAt: requireIsoDate(input.lastSeenAt, 'ralphFailureState.lastSeenAt'),
318
+ escalationStatus,
319
+ comparisonStatus: requireString(input.comparisonStatus, 'ralphFailureState.comparisonStatus', { max: 40 }),
320
+ acceptedRiskRef: optionalString(input.acceptedRiskRef, 'ralphFailureState.acceptedRiskRef', { max: 160, pattern: UUIDISH }),
321
+ nextAction: requireString(input.nextAction, 'ralphFailureState.nextAction', { max: 700 }),
322
+ };
323
+ if (record.attemptCount < 0) throw new Error('ralphFailureState.attemptCount must be zero or greater');
324
+ if (record.escalationStatus === 'acceptedRisk' && !record.acceptedRiskRef) {
325
+ throw new Error('acceptedRisk failure records require acceptedRiskRef');
326
+ }
327
+ assertSourceSafePayload(record, 'ralphFailureState.record');
328
+ return record;
329
+ }
330
+
331
+ export function validateRalphFailureStateArtifact(input = {}) {
332
+ assertSourceSafePayload(input, 'ralphFailureStateArtifact');
333
+ const artifact = {
334
+ schemaVersion: requireString(input.schemaVersion, 'ralphFailureState.schemaVersion', { max: 80, pattern: UUIDISH }),
335
+ generatedAt: requireIsoDate(input.generatedAt, 'ralphFailureState.generatedAt'),
336
+ root: cloneSourceSafe(input.root || {}, 'ralphFailureState.root'),
337
+ sources: cloneSourceSafe(input.sources || {}, 'ralphFailureState.sources'),
338
+ retryConfig: normalizeRetryConfig(input.retryConfig || DEFAULT_RALPH_LOOP_RETRY),
339
+ summary: cloneSourceSafe(input.summary || {}, 'ralphFailureState.summary'),
340
+ reportHandoff: cloneSourceSafe(input.reportHandoff || {}, 'ralphFailureState.reportHandoff'),
341
+ failures: asArray(input.failures, 'ralphFailureState.failures').map(validateFailureRecord),
342
+ privacy: cloneSourceSafe(input.privacy || {}, 'ralphFailureState.privacy'),
343
+ };
344
+ if (artifact.schemaVersion !== RALPH_FAILURE_STATE_SCHEMA_VERSION) {
345
+ throw new Error(`ralphFailureState.schemaVersion must be ${RALPH_FAILURE_STATE_SCHEMA_VERSION}`);
346
+ }
347
+ return artifact;
348
+ }
349
+
350
+ export function hashRalphFailureStateArtifact(artifact) {
351
+ assertSourceSafePayload(artifact, 'ralphFailureStateArtifact');
352
+ return sha256(stableStringify(validateRalphFailureStateArtifact(artifact)));
353
+ }
354
+
355
+ async function readLocalArtifact({ rootPath, ref, type }) {
356
+ if (!ref || ref.type !== type || ref.storage !== 'local' || !ref.uri) {
357
+ throw new Error(`Ralph failure tracking requires a local ${type} artifact reference`);
358
+ }
359
+ const resolvedRoot = path.resolve(rootPath);
360
+ const target = path.resolve(resolvedRoot, ref.uri);
361
+ const relative = path.relative(resolvedRoot, target);
362
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
363
+ throw new Error(`Ralph failure tracking artifact ref for ${type} escapes the bound root`);
364
+ }
365
+ const raw = await fs.readFile(target, 'utf8');
366
+ if (!ref.hash) {
367
+ throw new Error(`Ralph failure tracking requires ${type} artifact refs to include a hash`);
368
+ }
369
+ const actualHash = sha256(raw);
370
+ if (actualHash.toLowerCase() !== String(ref.hash).toLowerCase()) {
371
+ throw new Error(`Ralph failure tracking ${type} artifact hash mismatch: expected ${ref.hash}, got ${actualHash}`);
372
+ }
373
+ const parsed = JSON.parse(raw);
374
+ assertSourceSafePayload(parsed, `ralphFailureState.${type}`);
375
+ return parsed;
376
+ }
377
+
378
+ function findArtifactRef(state, type, preferredKeys = []) {
379
+ const artifacts = state?.artifacts || {};
380
+ for (const key of preferredKeys) {
381
+ if (artifacts[key]?.type === type) return artifacts[key];
382
+ }
383
+ return Object.values(artifacts).find((ref) => ref?.type === type) || null;
384
+ }
385
+
386
+ function normalizeRef(input = null, type, fallbackId) {
387
+ if (!input || typeof input !== 'object') return null;
388
+ if (!input.uri || !input.hash) return null;
389
+ return {
390
+ id: input.id || fallbackId,
391
+ type,
392
+ storage: input.storage || 'local',
393
+ uri: input.uri,
394
+ hash: input.hash,
395
+ };
396
+ }
397
+
398
+ export async function writeRalphFailureStateArtifact({ rootPath, runId, artifact }) {
399
+ const safeArtifact = validateRalphFailureStateArtifact(artifact);
400
+ const safeRunId = validateRunIdSegment(runId, 'runId');
401
+ const relativeUri = `.vibesecur/deep-scans/${safeRunId}/${FAILURE_STATE_FILE}`;
402
+ const target = path.join(path.resolve(rootPath), relativeUri);
403
+ await fs.mkdir(path.dirname(target), { recursive: true });
404
+ const serialized = `${JSON.stringify(stableSort(safeArtifact), null, 2)}\n`;
405
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
406
+ await fs.writeFile(tmp, serialized, 'utf8');
407
+ await fs.rename(tmp, target);
408
+ return {
409
+ uri: relativeUri,
410
+ hash: sha256(serialized),
411
+ };
412
+ }
413
+
414
+ export function ralphFailureTrackStepper() {
415
+ return {
416
+ id: RALPH_FAILURE_STATE_STEPPER_ID,
417
+ version: RALPH_FAILURE_STATE_STEPPER_VERSION,
418
+ title: 'Ralph Failure Tracking Stepper',
419
+ category: 'ralph_loop',
420
+ requiredInputs: ['ralph_comparison'],
421
+ producedArtifacts: ['ralph_failure_state'],
422
+ defaultTimeoutMs: 30000,
423
+ async run({ runId, config = {}, state = {}, tools = {} }) {
424
+ const rootPath = tools.rootPath || config.rootPath || '.';
425
+ const startedAt = nowIso();
426
+ const comparisonRef = findArtifactRef(state, 'ralph_comparison', ['ralph.compare']);
427
+ if (!comparisonRef) {
428
+ return {
429
+ stepperId: RALPH_FAILURE_STATE_STEPPER_ID,
430
+ version: RALPH_FAILURE_STATE_STEPPER_VERSION,
431
+ status: 'skipped',
432
+ startedAt,
433
+ finishedAt: nowIso(),
434
+ evidence: [],
435
+ findings: [],
436
+ artifacts: [],
437
+ receipts: [],
438
+ summary: 'Ralph failure tracking skipped because no ralph_comparison artifact was available.',
439
+ skippedReason: 'Missing ralph_comparison artifact from ralph.compare.',
440
+ nextActions: ['Run ralph.compare with a baseline deterministic scan before tracking repeated failures.'],
441
+ };
442
+ }
443
+
444
+ const compareStep = state.stepResults?.['ralph.compare'];
445
+ if (compareStep?.status === 'skipped') {
446
+ return {
447
+ stepperId: RALPH_FAILURE_STATE_STEPPER_ID,
448
+ version: RALPH_FAILURE_STATE_STEPPER_VERSION,
449
+ status: 'skipped',
450
+ startedAt,
451
+ finishedAt: nowIso(),
452
+ evidence: [],
453
+ findings: [],
454
+ artifacts: [],
455
+ receipts: [],
456
+ summary: 'Ralph failure tracking skipped because comparison was skipped.',
457
+ skippedReason: compareStep.skippedReason || 'ralph.compare did not produce comparison evidence.',
458
+ nextActions: ['Provide previousDeterministicScanRef on rerun to enable comparison and failure tracking.'],
459
+ };
460
+ }
461
+
462
+ const tasksRef = findArtifactRef(state, 'agent_fix_tasks', ['ralph.tasks']);
463
+ const previousRef = normalizeRef(config.previousFailureStateRef, 'ralph_failure_state', 'artifact-previous-failure-state');
464
+ const comparisonArtifact = await readLocalArtifact({
465
+ rootPath,
466
+ ref: comparisonRef,
467
+ type: 'ralph_comparison',
468
+ });
469
+ const previousFailureState = previousRef
470
+ ? await readLocalArtifact({
471
+ rootPath,
472
+ ref: previousRef,
473
+ type: 'ralph_failure_state',
474
+ })
475
+ : {};
476
+ const agentFixTasks = tasksRef
477
+ ? await readLocalArtifact({
478
+ rootPath,
479
+ ref: tasksRef,
480
+ type: 'agent_fix_tasks',
481
+ })
482
+ : { tasks: [] };
483
+
484
+ const retryConfig = normalizeRetryConfig(
485
+ config.retry
486
+ || state.graphProfile?.ralphLoop?.retry
487
+ || DEFAULT_RALPH_LOOP_RETRY,
488
+ );
489
+
490
+ const artifact = buildRalphFailureStateArtifact({
491
+ comparisonArtifact,
492
+ previousFailureState,
493
+ agentFixTasks,
494
+ retryConfig,
495
+ generatedAt: config.generatedAt || startedAt,
496
+ sourceRefs: {
497
+ ralphComparison: comparisonRef.id,
498
+ previousFailureState: previousRef?.id || null,
499
+ agentFixTasks: tasksRef?.id || null,
500
+ },
501
+ });
502
+ const contentHash = hashRalphFailureStateArtifact(artifact);
503
+ const written = await writeRalphFailureStateArtifact({ rootPath, runId, artifact });
504
+ const safeRunId = validateRunIdSegment(runId, 'runId');
505
+ const ref = createArtifactRef({
506
+ id: `artifact-${safeRunId}-ralph-failure-state`,
507
+ type: 'ralph_failure_state',
508
+ storage: 'local',
509
+ uri: written.uri,
510
+ hash: written.hash,
511
+ preview: 'Ralph repeated failure state with attempt counts and escalation metadata only.',
512
+ metadata: {
513
+ schemaVersion: RALPH_FAILURE_STATE_SCHEMA_VERSION,
514
+ contentHash,
515
+ generatedBy: RALPH_FAILURE_STATE_STEPPER_ID,
516
+ activeFailureCount: artifact.summary.activeFailureCount,
517
+ repeatedFailureCount: artifact.summary.repeatedFailureCount,
518
+ escalatedCount: artifact.summary.escalatedCount,
519
+ },
520
+ });
521
+
522
+ return {
523
+ stepperId: RALPH_FAILURE_STATE_STEPPER_ID,
524
+ version: RALPH_FAILURE_STATE_STEPPER_VERSION,
525
+ status: 'passed',
526
+ startedAt,
527
+ finishedAt: nowIso(),
528
+ evidence: [{
529
+ type: 'hash',
530
+ label: 'Ralph failure state artifact hash',
531
+ hash: ref.hash,
532
+ preview: 'Repeated failure tracking artifact written locally.',
533
+ summary: `${artifact.summary.repeatedFailureCount} repeated failure(s), ${artifact.summary.escalatedCount} escalated.`,
534
+ metadata: {
535
+ artifactId: ref.id,
536
+ contentHash,
537
+ ralphComparisonArtifactId: comparisonRef.id,
538
+ },
539
+ }],
540
+ findings: [],
541
+ artifacts: [ref],
542
+ receipts: [],
543
+ summary: `Ralph failure tracking generated: ${artifact.summary.activeFailureCount} active failure(s), ${artifact.summary.repeatedFailureCount} repeated, ${artifact.summary.escalatedCount} escalated.`,
544
+ nextActions: ['Use ralph-failure-state report handoff fields for release governance and human review decisions.'],
545
+ };
546
+ },
547
+ };
548
+ }