cipher-security 2.0.8 → 2.2.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/bin/cipher.js +11 -1
- package/lib/agent-runtime/handlers/architect.js +199 -0
- package/lib/agent-runtime/handlers/base.js +240 -0
- package/lib/agent-runtime/handlers/blue.js +220 -0
- package/lib/agent-runtime/handlers/incident.js +161 -0
- package/lib/agent-runtime/handlers/privacy.js +190 -0
- package/lib/agent-runtime/handlers/purple.js +209 -0
- package/lib/agent-runtime/handlers/recon.js +174 -0
- package/lib/agent-runtime/handlers/red.js +246 -0
- package/lib/agent-runtime/handlers/researcher.js +170 -0
- package/lib/agent-runtime/handlers.js +35 -0
- package/lib/agent-runtime/index.js +196 -0
- package/lib/agent-runtime/parser.js +316 -0
- package/lib/analyze/consistency.js +566 -0
- package/lib/analyze/constitution.js +110 -0
- package/lib/analyze/sharding.js +251 -0
- package/lib/autonomous/agent-tool.js +165 -0
- package/lib/autonomous/feedback-loop.js +13 -6
- package/lib/autonomous/framework.js +17 -0
- package/lib/autonomous/handoff.js +506 -0
- package/lib/autonomous/modes/blue.js +26 -0
- package/lib/autonomous/modes/red.js +585 -0
- package/lib/autonomous/modes/researcher.js +322 -0
- package/lib/autonomous/researcher.js +12 -45
- package/lib/autonomous/runner.js +9 -537
- package/lib/benchmark/agent.js +88 -26
- package/lib/benchmark/baselines.js +3 -0
- package/lib/benchmark/claude-code-solver.js +254 -0
- package/lib/benchmark/cognitive.js +283 -0
- package/lib/benchmark/index.js +12 -2
- package/lib/benchmark/knowledge.js +281 -0
- package/lib/benchmark/llm.js +156 -15
- package/lib/benchmark/models.js +5 -2
- package/lib/benchmark/nyu-ctf.js +192 -0
- package/lib/benchmark/overthewire.js +347 -0
- package/lib/benchmark/picoctf.js +281 -0
- package/lib/benchmark/prompts.js +280 -0
- package/lib/benchmark/registry.js +219 -0
- package/lib/benchmark/remote-solver.js +356 -0
- package/lib/benchmark/remote-target.js +263 -0
- package/lib/benchmark/reporter.js +35 -0
- package/lib/benchmark/runner.js +174 -10
- package/lib/benchmark/sandbox.js +35 -0
- package/lib/benchmark/scorer.js +22 -4
- package/lib/benchmark/solver.js +34 -1
- package/lib/benchmark/tools.js +262 -16
- package/lib/commands.js +9 -0
- package/lib/execution/council.js +434 -0
- package/lib/execution/parallel.js +292 -0
- package/lib/gates/circuit-breaker.js +135 -0
- package/lib/gates/confidence.js +302 -0
- package/lib/gates/corrections.js +219 -0
- package/lib/gates/self-check.js +245 -0
- package/lib/gateway/commands.js +727 -0
- package/lib/guardrails/engine.js +364 -0
- package/lib/mcp/server.js +349 -3
- package/lib/memory/compressor.js +94 -7
- package/lib/pipeline/hooks.js +288 -0
- package/lib/pipeline/index.js +11 -0
- package/lib/review/budget.js +210 -0
- package/lib/review/engine.js +526 -0
- package/lib/review/layers/acceptance-auditor.js +279 -0
- package/lib/review/layers/blind-hunter.js +500 -0
- package/lib/review/layers/defense-in-depth.js +209 -0
- package/lib/review/layers/edge-case-hunter.js +266 -0
- package/lib/review/panel.js +519 -0
- package/lib/review/two-stage.js +244 -0
- package/lib/session/cost-tracker.js +203 -0
- package/lib/session/logger.js +349 -0
- package/package.json +1 -1
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parallel Assessment Engine — wave-based concurrent security assessment.
|
|
7
|
+
*
|
|
8
|
+
* Dispatches independent domain assessments in parallel (wave 1),
|
|
9
|
+
* then cross-references results in a synthesis phase (wave 2).
|
|
10
|
+
*
|
|
11
|
+
* Pattern: Superpowers subagent dispatch + SuperClaude parallel execution.
|
|
12
|
+
*
|
|
13
|
+
* @module execution/parallel
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Assessment result types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} DomainAssessment
|
|
22
|
+
* @property {string} domain — Assessment domain
|
|
23
|
+
* @property {string} status — 'completed' | 'failed' | 'skipped'
|
|
24
|
+
* @property {Object} findings — Domain-specific findings
|
|
25
|
+
* @property {number} durationMs — Execution time
|
|
26
|
+
* @property {string|null} error — Error message if failed
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} BriefingResult
|
|
31
|
+
* @property {DomainAssessment[]} assessments — Individual domain results
|
|
32
|
+
* @property {Object} synthesis — Cross-domain synthesis
|
|
33
|
+
* @property {number} totalDurationMs — Total wall-clock time
|
|
34
|
+
* @property {{ completed: number, failed: number, skipped: number }} summary
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// ParallelAssessor
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
export class ParallelAssessor {
|
|
42
|
+
/**
|
|
43
|
+
* @param {{ assessors?: Record<string, Function>, timeoutMs?: number }} opts
|
|
44
|
+
*/
|
|
45
|
+
constructor(opts = {}) {
|
|
46
|
+
this._assessors = opts.assessors || {};
|
|
47
|
+
this._timeoutMs = opts.timeoutMs || 30000;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register a domain assessor function.
|
|
52
|
+
* @param {string} domain — Domain name (e.g., 'attack_surface', 'threat_profile')
|
|
53
|
+
* @param {Function} fn — Async function(target, context) → findings object
|
|
54
|
+
*/
|
|
55
|
+
register(domain, fn) {
|
|
56
|
+
this._assessors[domain] = fn;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run all registered assessors in parallel (Wave 1), then synthesize (Wave 2).
|
|
61
|
+
* @param {string} target — Assessment target
|
|
62
|
+
* @param {Object} [context] — Additional context
|
|
63
|
+
* @returns {Promise<BriefingResult>}
|
|
64
|
+
*/
|
|
65
|
+
async assess(target, context = {}) {
|
|
66
|
+
const startTime = Date.now();
|
|
67
|
+
|
|
68
|
+
// Wave 1: Parallel domain assessments
|
|
69
|
+
const domains = Object.keys(this._assessors);
|
|
70
|
+
const wave1 = await Promise.allSettled(
|
|
71
|
+
domains.map(async (domain) => {
|
|
72
|
+
const domainStart = Date.now();
|
|
73
|
+
try {
|
|
74
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
75
|
+
setTimeout(() => reject(new Error(`Timeout after ${this._timeoutMs}ms`)), this._timeoutMs)
|
|
76
|
+
);
|
|
77
|
+
const findings = await Promise.race([
|
|
78
|
+
this._assessors[domain](target, context),
|
|
79
|
+
timeoutPromise,
|
|
80
|
+
]);
|
|
81
|
+
return {
|
|
82
|
+
domain,
|
|
83
|
+
status: 'completed',
|
|
84
|
+
findings: findings || {},
|
|
85
|
+
durationMs: Date.now() - domainStart,
|
|
86
|
+
error: null,
|
|
87
|
+
};
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return {
|
|
90
|
+
domain,
|
|
91
|
+
status: 'failed',
|
|
92
|
+
findings: {},
|
|
93
|
+
durationMs: Date.now() - domainStart,
|
|
94
|
+
error: err.message,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const assessments = wave1.map(r =>
|
|
101
|
+
r.status === 'fulfilled' ? r.value : {
|
|
102
|
+
domain: 'unknown',
|
|
103
|
+
status: 'failed',
|
|
104
|
+
findings: {},
|
|
105
|
+
durationMs: 0,
|
|
106
|
+
error: r.reason?.message || 'Unknown error',
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Wave 2: Cross-domain synthesis
|
|
111
|
+
const synthesis = this._synthesize(assessments, target);
|
|
112
|
+
|
|
113
|
+
const totalDurationMs = Date.now() - startTime;
|
|
114
|
+
const completed = assessments.filter(a => a.status === 'completed').length;
|
|
115
|
+
const failed = assessments.filter(a => a.status === 'failed').length;
|
|
116
|
+
const skipped = assessments.filter(a => a.status === 'skipped').length;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
assessments,
|
|
120
|
+
synthesis,
|
|
121
|
+
totalDurationMs,
|
|
122
|
+
summary: { completed, failed, skipped },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Cross-reference domain assessments and produce synthesis.
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
_synthesize(assessments, target) {
|
|
131
|
+
const completed = assessments.filter(a => a.status === 'completed');
|
|
132
|
+
if (completed.length === 0) {
|
|
133
|
+
return { target, gaps: [], priorities: [], crossReferences: [], overallRisk: 'unknown' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Cross-reference: find gaps between attack surface and detection coverage
|
|
137
|
+
const attackFindings = completed.find(a => a.domain === 'attack_surface')?.findings || {};
|
|
138
|
+
const detectionFindings = completed.find(a => a.domain === 'detection_coverage')?.findings || {};
|
|
139
|
+
|
|
140
|
+
const gaps = [];
|
|
141
|
+
if (attackFindings.techniques && detectionFindings.coveredTechniques) {
|
|
142
|
+
const uncovered = (attackFindings.techniques || []).filter(
|
|
143
|
+
t => !(detectionFindings.coveredTechniques || []).includes(t)
|
|
144
|
+
);
|
|
145
|
+
for (const t of uncovered) {
|
|
146
|
+
gaps.push({ technique: t, type: 'detection_gap', severity: 'high' });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Prioritize by cross-domain risk
|
|
151
|
+
const priorities = completed
|
|
152
|
+
.flatMap(a => Object.entries(a.findings).map(([k, v]) => ({ domain: a.domain, finding: k, value: v })))
|
|
153
|
+
.slice(0, 20);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
target,
|
|
157
|
+
gaps,
|
|
158
|
+
priorities,
|
|
159
|
+
crossReferences: completed.map(a => a.domain),
|
|
160
|
+
overallRisk: gaps.length > 5 ? 'critical' : gaps.length > 2 ? 'high' : gaps.length > 0 ? 'medium' : 'low',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Engagement Constitution
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @typedef {Object} Constitution
|
|
171
|
+
* @property {string} engagementId
|
|
172
|
+
* @property {string} target
|
|
173
|
+
* @property {Object} scope — { inScope: string[], outOfScope: string[] }
|
|
174
|
+
* @property {Object} rulesOfEngagement — { passiveOnly: boolean, activeScanAllowed: boolean, exploitAllowed: boolean, socialEngAllowed: boolean }
|
|
175
|
+
* @property {string[]} complianceFrameworks — Applicable frameworks
|
|
176
|
+
* @property {Object} severityClassification — Custom severity criteria
|
|
177
|
+
* @property {string} reportingFormat — 'json' | 'markdown' | 'sarif'
|
|
178
|
+
* @property {string} createdAt
|
|
179
|
+
*/
|
|
180
|
+
|
|
181
|
+
export class EngagementConstitution {
|
|
182
|
+
/**
|
|
183
|
+
* @param {Partial<Constitution>} opts
|
|
184
|
+
*/
|
|
185
|
+
constructor(opts = {}) {
|
|
186
|
+
this.engagementId = opts.engagementId || `ENG-${Date.now().toString(36)}`;
|
|
187
|
+
this.target = opts.target || '';
|
|
188
|
+
this.scope = opts.scope || { inScope: [], outOfScope: [] };
|
|
189
|
+
this.rulesOfEngagement = opts.rulesOfEngagement || {
|
|
190
|
+
passiveOnly: false,
|
|
191
|
+
activeScanAllowed: true,
|
|
192
|
+
exploitAllowed: false,
|
|
193
|
+
socialEngAllowed: false,
|
|
194
|
+
};
|
|
195
|
+
this.complianceFrameworks = opts.complianceFrameworks || [];
|
|
196
|
+
this.severityClassification = opts.severityClassification || {
|
|
197
|
+
critical: 'RCE, auth bypass, data exfiltration',
|
|
198
|
+
high: 'Privilege escalation, sensitive data exposure',
|
|
199
|
+
medium: 'Information disclosure, CSRF',
|
|
200
|
+
low: 'Verbose errors, missing headers',
|
|
201
|
+
info: 'Informational findings, best practice deviations',
|
|
202
|
+
};
|
|
203
|
+
this.reportingFormat = opts.reportingFormat || 'markdown';
|
|
204
|
+
this.createdAt = opts.createdAt || new Date().toISOString();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Validate an output against the constitution.
|
|
209
|
+
* @param {Object} output — Security assessment output to validate
|
|
210
|
+
* @returns {{ valid: boolean, violations: string[] }}
|
|
211
|
+
*/
|
|
212
|
+
validate(output) {
|
|
213
|
+
const violations = [];
|
|
214
|
+
|
|
215
|
+
// Check scope compliance
|
|
216
|
+
if (output.target && this.scope.outOfScope.length > 0) {
|
|
217
|
+
for (const excluded of this.scope.outOfScope) {
|
|
218
|
+
if (output.target.includes(excluded)) {
|
|
219
|
+
violations.push(`Target "${output.target}" is out of scope (excluded: ${excluded})`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check RoE compliance
|
|
225
|
+
if (output.technique) {
|
|
226
|
+
if (this.rulesOfEngagement.passiveOnly && output.technique.includes('exploit')) {
|
|
227
|
+
violations.push(`Exploitation techniques not allowed (passive-only engagement)`);
|
|
228
|
+
}
|
|
229
|
+
if (!this.rulesOfEngagement.exploitAllowed && output.technique.includes('exploit')) {
|
|
230
|
+
violations.push(`Exploitation not authorized in rules of engagement`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { valid: violations.length === 0, violations };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Serialize to markdown.
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
toMarkdown() {
|
|
242
|
+
const lines = [
|
|
243
|
+
`# Engagement Constitution: ${this.engagementId}`,
|
|
244
|
+
'',
|
|
245
|
+
`**Target:** ${this.target}`,
|
|
246
|
+
`**Created:** ${this.createdAt}`,
|
|
247
|
+
`**Reporting Format:** ${this.reportingFormat}`,
|
|
248
|
+
'',
|
|
249
|
+
'## Scope',
|
|
250
|
+
'',
|
|
251
|
+
'### In Scope',
|
|
252
|
+
...this.scope.inScope.map(s => `- ${s}`),
|
|
253
|
+
'',
|
|
254
|
+
'### Out of Scope',
|
|
255
|
+
...this.scope.outOfScope.map(s => `- ${s}`),
|
|
256
|
+
'',
|
|
257
|
+
'## Rules of Engagement',
|
|
258
|
+
'',
|
|
259
|
+
`- Passive only: ${this.rulesOfEngagement.passiveOnly ? 'Yes' : 'No'}`,
|
|
260
|
+
`- Active scanning: ${this.rulesOfEngagement.activeScanAllowed ? 'Allowed' : 'Not allowed'}`,
|
|
261
|
+
`- Exploitation: ${this.rulesOfEngagement.exploitAllowed ? 'Allowed' : 'Not allowed'}`,
|
|
262
|
+
`- Social engineering: ${this.rulesOfEngagement.socialEngAllowed ? 'Allowed' : 'Not allowed'}`,
|
|
263
|
+
'',
|
|
264
|
+
'## Compliance Frameworks',
|
|
265
|
+
'',
|
|
266
|
+
...(this.complianceFrameworks.length > 0 ? this.complianceFrameworks.map(f => `- ${f}`) : ['- None specified']),
|
|
267
|
+
'',
|
|
268
|
+
'## Severity Classification',
|
|
269
|
+
'',
|
|
270
|
+
...Object.entries(this.severityClassification).map(([level, desc]) => `- **${level}:** ${desc}`),
|
|
271
|
+
'',
|
|
272
|
+
];
|
|
273
|
+
return lines.join('\n');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Serialize to JSON.
|
|
278
|
+
* @returns {Object}
|
|
279
|
+
*/
|
|
280
|
+
toDict() {
|
|
281
|
+
return {
|
|
282
|
+
engagementId: this.engagementId,
|
|
283
|
+
target: this.target,
|
|
284
|
+
scope: this.scope,
|
|
285
|
+
rulesOfEngagement: this.rulesOfEngagement,
|
|
286
|
+
complianceFrameworks: this.complianceFrameworks,
|
|
287
|
+
severityClassification: this.severityClassification,
|
|
288
|
+
reportingFormat: this.reportingFormat,
|
|
289
|
+
createdAt: this.createdAt,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Circuit Breaker — 3-strike architecture rule.
|
|
7
|
+
*
|
|
8
|
+
* Tracks consecutive failures per issue ID. After 3 failed attempts
|
|
9
|
+
* on the same issue, halts execution and surfaces an architecture
|
|
10
|
+
* review question instead of attempting Fix #4.
|
|
11
|
+
*
|
|
12
|
+
* Source: Superpowers systematic-debugging "Three-Fix Architecture Questioning Rule"
|
|
13
|
+
*
|
|
14
|
+
* @module gates/circuit-breaker
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// ThreeStrikeBreaker
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} IssueState
|
|
23
|
+
* @property {number} consecutiveFailures — Current streak of failures
|
|
24
|
+
* @property {string[]} failureDescriptions — Description of each failure
|
|
25
|
+
* @property {string} lastAttempt — ISO timestamp of last attempt
|
|
26
|
+
* @property {number} totalAttempts — Total attempts (including successes)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export class ThreeStrikeBreaker {
|
|
30
|
+
/**
|
|
31
|
+
* @param {{ threshold?: number }} [opts]
|
|
32
|
+
*/
|
|
33
|
+
constructor(opts = {}) {
|
|
34
|
+
/** @type {Map<string, IssueState>} */
|
|
35
|
+
this._issues = new Map();
|
|
36
|
+
this._threshold = opts.threshold ?? 3;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Record an attempt on an issue.
|
|
41
|
+
* @param {string} issueId — Unique identifier for the issue
|
|
42
|
+
* @param {boolean} success — Whether the attempt succeeded
|
|
43
|
+
* @param {string} [description] — Description of what was tried
|
|
44
|
+
*/
|
|
45
|
+
recordAttempt(issueId, success, description = '') {
|
|
46
|
+
let state = this._issues.get(issueId);
|
|
47
|
+
if (!state) {
|
|
48
|
+
state = { consecutiveFailures: 0, failureDescriptions: [], lastAttempt: '', totalAttempts: 0 };
|
|
49
|
+
this._issues.set(issueId, state);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
state.totalAttempts++;
|
|
53
|
+
state.lastAttempt = new Date().toISOString();
|
|
54
|
+
|
|
55
|
+
if (success) {
|
|
56
|
+
state.consecutiveFailures = 0;
|
|
57
|
+
state.failureDescriptions = [];
|
|
58
|
+
} else {
|
|
59
|
+
state.consecutiveFailures++;
|
|
60
|
+
if (description) {
|
|
61
|
+
state.failureDescriptions.push(description);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if the circuit breaker should halt for this issue.
|
|
68
|
+
* @param {string} issueId
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
shouldHalt(issueId) {
|
|
72
|
+
const state = this._issues.get(issueId);
|
|
73
|
+
if (!state) return false;
|
|
74
|
+
return state.consecutiveFailures >= this._threshold;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the architecture review question for a halted issue.
|
|
79
|
+
* @param {string} issueId
|
|
80
|
+
* @returns {{ halted: boolean, question: string, failures: string[], totalAttempts: number }}
|
|
81
|
+
*/
|
|
82
|
+
getArchitectureQuestion(issueId) {
|
|
83
|
+
const state = this._issues.get(issueId);
|
|
84
|
+
if (!state || state.consecutiveFailures < this._threshold) {
|
|
85
|
+
return { halted: false, question: '', failures: [], totalAttempts: 0 };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const failureList = state.failureDescriptions
|
|
89
|
+
.map((d, i) => ` ${i + 1}. ${d}`)
|
|
90
|
+
.join('\n');
|
|
91
|
+
|
|
92
|
+
const question = [
|
|
93
|
+
`CIRCUIT BREAKER: ${this._threshold} consecutive failures on "${issueId}".`,
|
|
94
|
+
'',
|
|
95
|
+
'Failed attempts:',
|
|
96
|
+
failureList || ' (no descriptions recorded)',
|
|
97
|
+
'',
|
|
98
|
+
'Before attempting another fix, answer these questions:',
|
|
99
|
+
' 1. Is this pattern fundamentally sound?',
|
|
100
|
+
' 2. Are we sticking with it through sheer inertia?',
|
|
101
|
+
' 3. Should we refactor architecture vs. continue fixing symptoms?',
|
|
102
|
+
'',
|
|
103
|
+
'Discuss with human before attempting more fixes.',
|
|
104
|
+
].join('\n');
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
halted: true,
|
|
108
|
+
question,
|
|
109
|
+
failures: state.failureDescriptions,
|
|
110
|
+
totalAttempts: state.totalAttempts,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Reset tracking for an issue.
|
|
116
|
+
* @param {string} issueId
|
|
117
|
+
*/
|
|
118
|
+
reset(issueId) {
|
|
119
|
+
this._issues.delete(issueId);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get current state for an issue.
|
|
124
|
+
* @param {string} issueId
|
|
125
|
+
* @returns {IssueState|null}
|
|
126
|
+
*/
|
|
127
|
+
getState(issueId) {
|
|
128
|
+
return this._issues.get(issueId) || null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Number of tracked issues */
|
|
132
|
+
get size() {
|
|
133
|
+
return this._issues.size;
|
|
134
|
+
}
|
|
135
|
+
}
|