@wrongstack/plugins 1.0.13 → 1.0.15
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 +2 -1
- package/dist/accessibility-auditor.js +1 -1
- package/dist/auto-doc.js +4 -0
- package/dist/auto-i18n-extractor.js +1 -1
- package/dist/code-metrics.js +1 -1
- package/dist/cron.js +10 -1
- package/dist/dead-code-detector.js +2 -2
- package/dist/duplicate-code-detector.js +2 -2
- package/dist/feature-flag-tracker.js +1 -1
- package/dist/git-autocommit.js +2 -1
- package/dist/index.js +204 -168
- package/dist/interface-contract-guard.js +1 -1
- package/dist/loop-breaker.js +163 -150
- package/dist/notify-hub.js +12 -3
- package/dist/refactor-suggester.js +2 -2
- package/dist/security-hotspot-scanner.js +2 -2
- package/package.json +5 -5
package/dist/loop-breaker.js
CHANGED
|
@@ -1,29 +1,70 @@
|
|
|
1
1
|
// src/loop-breaker/index.ts
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { isAbsolute, relative } from "node:path";
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
4
|
+
var FALLBACK_SESSION = "__default__";
|
|
5
|
+
var MAX_TRACKED_RUNS = 32;
|
|
6
|
+
function createRunState() {
|
|
7
|
+
return {
|
|
8
|
+
lastFingerprint: null,
|
|
9
|
+
streak: 0,
|
|
10
|
+
recent: [],
|
|
11
|
+
pendingBlockReason: null,
|
|
12
|
+
lastDiffFingerprint: null,
|
|
13
|
+
noDiffStreak: 0,
|
|
14
|
+
lastErrorFingerprint: null,
|
|
15
|
+
repeatedErrorStreak: 0,
|
|
16
|
+
invocations: 0,
|
|
17
|
+
postInvocations: 0,
|
|
18
|
+
warnings: 0,
|
|
19
|
+
blocks: 0,
|
|
20
|
+
oscillationsDetected: 0,
|
|
21
|
+
stepBudgetBlocks: 0,
|
|
22
|
+
noDiffWarnings: 0,
|
|
23
|
+
noDiffBlocks: 0,
|
|
24
|
+
repeatedErrorWarnings: 0,
|
|
25
|
+
repeatedErrorBlocks: 0
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
var state = { runs: /* @__PURE__ */ new Map(), hookUnregister: null };
|
|
29
|
+
function sessionKey(sessionId) {
|
|
30
|
+
return sessionId || FALLBACK_SESSION;
|
|
31
|
+
}
|
|
32
|
+
function storeRun(key, current) {
|
|
33
|
+
state.runs.set(key, current);
|
|
34
|
+
if (state.runs.size <= MAX_TRACKED_RUNS) return;
|
|
35
|
+
const oldest = state.runs.keys().next().value;
|
|
36
|
+
if (oldest !== void 0 && oldest !== key) state.runs.delete(oldest);
|
|
37
|
+
}
|
|
38
|
+
function runState(sessionId) {
|
|
39
|
+
const key = sessionKey(sessionId);
|
|
40
|
+
let current = state.runs.get(key);
|
|
41
|
+
if (!current) {
|
|
42
|
+
current = createRunState();
|
|
43
|
+
storeRun(key, current);
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
function resetRun(sessionId) {
|
|
48
|
+
storeRun(sessionKey(sessionId), createRunState());
|
|
49
|
+
}
|
|
50
|
+
function aggregateRuns() {
|
|
51
|
+
const total = createRunState();
|
|
52
|
+
for (const current of state.runs.values()) {
|
|
53
|
+
total.invocations += current.invocations;
|
|
54
|
+
total.postInvocations += current.postInvocations;
|
|
55
|
+
total.warnings += current.warnings;
|
|
56
|
+
total.blocks += current.blocks;
|
|
57
|
+
total.oscillationsDetected += current.oscillationsDetected;
|
|
58
|
+
total.stepBudgetBlocks += current.stepBudgetBlocks;
|
|
59
|
+
total.noDiffWarnings += current.noDiffWarnings;
|
|
60
|
+
total.noDiffBlocks += current.noDiffBlocks;
|
|
61
|
+
total.repeatedErrorWarnings += current.repeatedErrorWarnings;
|
|
62
|
+
total.repeatedErrorBlocks += current.repeatedErrorBlocks;
|
|
63
|
+
}
|
|
64
|
+
return total;
|
|
65
|
+
}
|
|
25
66
|
var DEFAULTS = {
|
|
26
|
-
enabled:
|
|
67
|
+
enabled: false,
|
|
27
68
|
// Documented contract (feature matrix, plugin description): warn, then
|
|
28
69
|
// block. A warn-only default meant an agent stuck re-issuing the same call
|
|
29
70
|
// was never actually stopped unless the user happened to set any option.
|
|
@@ -31,7 +72,7 @@ var DEFAULTS = {
|
|
|
31
72
|
warnAfter: 3,
|
|
32
73
|
blockAfter: 5,
|
|
33
74
|
oscillationWindow: 8,
|
|
34
|
-
maxSteps:
|
|
75
|
+
maxSteps: 0,
|
|
35
76
|
noDiffWarnAfter: 6,
|
|
36
77
|
noDiffBlockAfter: 10,
|
|
37
78
|
repeatedErrorWarnAfter: 2,
|
|
@@ -57,7 +98,7 @@ function readConfig(raw) {
|
|
|
57
98
|
const rawOsc = r["oscillationWindow"] ?? r["oscillation_window"] ?? r["window"];
|
|
58
99
|
const rawMaxSteps = r["maxSteps"] ?? r["max_steps"] ?? r["stepLimit"] ?? r["step_limit"];
|
|
59
100
|
return {
|
|
60
|
-
enabled: r["enabled"]
|
|
101
|
+
enabled: r["enabled"] === true,
|
|
61
102
|
mode,
|
|
62
103
|
warnAfter,
|
|
63
104
|
blockAfter,
|
|
@@ -170,7 +211,11 @@ var plugin = {
|
|
|
170
211
|
configSchema: {
|
|
171
212
|
type: "object",
|
|
172
213
|
properties: {
|
|
173
|
-
enabled: {
|
|
214
|
+
enabled: {
|
|
215
|
+
type: "boolean",
|
|
216
|
+
default: false,
|
|
217
|
+
description: "Opt-in master switch; disabled unless explicitly enabled."
|
|
218
|
+
},
|
|
174
219
|
mode: {
|
|
175
220
|
type: "string",
|
|
176
221
|
enum: ["warn", "block"],
|
|
@@ -198,8 +243,8 @@ var plugin = {
|
|
|
198
243
|
maxSteps: {
|
|
199
244
|
type: "number",
|
|
200
245
|
minimum: 0,
|
|
201
|
-
default:
|
|
202
|
-
description: "
|
|
246
|
+
default: 0,
|
|
247
|
+
description: "Optional maximum tool steps before blocking; 0 keeps runs unlimited."
|
|
203
248
|
},
|
|
204
249
|
noDiffWarnAfter: {
|
|
205
250
|
type: "number",
|
|
@@ -234,24 +279,7 @@ var plugin = {
|
|
|
234
279
|
}
|
|
235
280
|
},
|
|
236
281
|
setup(api) {
|
|
237
|
-
state.
|
|
238
|
-
state.streak = 0;
|
|
239
|
-
state.recent = [];
|
|
240
|
-
state.pendingBlockReason = null;
|
|
241
|
-
state.lastDiffFingerprint = null;
|
|
242
|
-
state.noDiffStreak = 0;
|
|
243
|
-
state.lastErrorFingerprint = null;
|
|
244
|
-
state.repeatedErrorStreak = 0;
|
|
245
|
-
state.invocations = 0;
|
|
246
|
-
state.postInvocations = 0;
|
|
247
|
-
state.warnings = 0;
|
|
248
|
-
state.blocks = 0;
|
|
249
|
-
state.oscillationsDetected = 0;
|
|
250
|
-
state.stepBudgetBlocks = 0;
|
|
251
|
-
state.noDiffWarnings = 0;
|
|
252
|
-
state.noDiffBlocks = 0;
|
|
253
|
-
state.repeatedErrorWarnings = 0;
|
|
254
|
-
state.repeatedErrorBlocks = 0;
|
|
282
|
+
state.runs.clear();
|
|
255
283
|
if (state.hookUnregister) {
|
|
256
284
|
try {
|
|
257
285
|
state.hookUnregister();
|
|
@@ -264,56 +292,57 @@ var plugin = {
|
|
|
264
292
|
if (!cfg.enabled) return;
|
|
265
293
|
const toolName = input.toolName ?? "unknown";
|
|
266
294
|
if (ignoreToolsSet.has(toolName)) return;
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
const reason =
|
|
270
|
-
|
|
271
|
-
|
|
295
|
+
const current = runState(input.sessionId);
|
|
296
|
+
if (current.pendingBlockReason && cfg.mode === "block") {
|
|
297
|
+
const reason = current.pendingBlockReason;
|
|
298
|
+
current.pendingBlockReason = null;
|
|
299
|
+
current.blocks += 1;
|
|
272
300
|
api.metrics.counter("blocks");
|
|
273
301
|
return { decision: "block", reason };
|
|
274
302
|
}
|
|
275
|
-
if (cfg.maxSteps > 0 &&
|
|
276
|
-
|
|
277
|
-
|
|
303
|
+
if (cfg.maxSteps > 0 && current.invocations >= cfg.maxSteps && cfg.mode === "block") {
|
|
304
|
+
current.blocks += 1;
|
|
305
|
+
current.stepBudgetBlocks += 1;
|
|
278
306
|
api.metrics.counter("blocks");
|
|
279
307
|
api.metrics.counter("step_budget_blocks");
|
|
280
308
|
return {
|
|
281
309
|
decision: "block",
|
|
282
|
-
reason: `loop-breaker: step budget exceeded (${
|
|
310
|
+
reason: `loop-breaker: step budget exceeded (${current.invocations}/${cfg.maxSteps} tool calls). Stop and report what has been tried instead of continuing an unbounded loop.`
|
|
283
311
|
};
|
|
284
312
|
}
|
|
313
|
+
current.invocations += 1;
|
|
285
314
|
const fp = fingerprint(toolName, input.toolInput);
|
|
286
|
-
if (fp ===
|
|
287
|
-
|
|
315
|
+
if (fp === current.lastFingerprint) {
|
|
316
|
+
current.streak += 1;
|
|
288
317
|
} else {
|
|
289
|
-
|
|
290
|
-
|
|
318
|
+
current.lastFingerprint = fp;
|
|
319
|
+
current.streak = 1;
|
|
291
320
|
}
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
321
|
+
current.recent.push(fp);
|
|
322
|
+
if (current.recent.length > Math.max(cfg.oscillationWindow, 16)) {
|
|
323
|
+
current.recent.splice(0, current.recent.length - Math.max(cfg.oscillationWindow, 16));
|
|
295
324
|
}
|
|
296
|
-
if (
|
|
297
|
-
|
|
325
|
+
if (current.streak >= cfg.blockAfter && cfg.mode === "block") {
|
|
326
|
+
current.blocks += 1;
|
|
298
327
|
api.metrics.counter("blocks");
|
|
299
328
|
return {
|
|
300
329
|
decision: "block",
|
|
301
|
-
reason: `loop-breaker: "${toolName}" has been called ${
|
|
330
|
+
reason: `loop-breaker: "${toolName}" has been called ${current.streak} times in a row with identical input. This looks like a runaway loop. Change the input, try a different tool, or explain to the user why repetition is needed. (Disable this guard via config.extensions["loop-breaker"].enabled = false.)`
|
|
302
331
|
};
|
|
303
332
|
}
|
|
304
|
-
if (
|
|
305
|
-
|
|
333
|
+
if (current.streak >= cfg.warnAfter) {
|
|
334
|
+
current.warnings += 1;
|
|
306
335
|
api.metrics.counter("warnings");
|
|
307
|
-
const remaining = cfg.mode === "block" ? cfg.blockAfter -
|
|
336
|
+
const remaining = cfg.mode === "block" ? cfg.blockAfter - current.streak : null;
|
|
308
337
|
return {
|
|
309
338
|
decision: "allow",
|
|
310
|
-
additionalContext: `loop-breaker: "${toolName}" repeated ${
|
|
339
|
+
additionalContext: `loop-breaker: "${toolName}" repeated ${current.streak}x with identical input.` + (remaining !== null && remaining > 0 ? ` It will be BLOCKED after ${remaining} more identical call(s).` : "") + " If the previous result was not what you needed, change the approach instead of retrying."
|
|
311
340
|
};
|
|
312
341
|
}
|
|
313
|
-
if (isOscillating(
|
|
314
|
-
|
|
342
|
+
if (isOscillating(current.recent, cfg.oscillationWindow)) {
|
|
343
|
+
current.oscillationsDetected += 1;
|
|
315
344
|
api.metrics.counter("oscillations");
|
|
316
|
-
|
|
345
|
+
current.recent = [];
|
|
317
346
|
return {
|
|
318
347
|
decision: "allow",
|
|
319
348
|
additionalContext: `loop-breaker: the last ${cfg.oscillationWindow} tool calls alternate between two identical calls (A-B-A-B pattern). You appear to be undoing and redoing the same work. Step back and pick a single approach.`
|
|
@@ -325,32 +354,33 @@ var plugin = {
|
|
|
325
354
|
if (!cfg.enabled) return;
|
|
326
355
|
const toolName = input.toolName ?? "unknown";
|
|
327
356
|
if (ignoreToolsSet.has(toolName)) return;
|
|
328
|
-
|
|
357
|
+
const current = runState(input.sessionId);
|
|
358
|
+
current.postInvocations += 1;
|
|
329
359
|
if (input.toolResult?.isError) {
|
|
330
360
|
const errorFingerprint = normalizeError(input.toolResult.content);
|
|
331
|
-
if (errorFingerprint && errorFingerprint ===
|
|
332
|
-
|
|
361
|
+
if (errorFingerprint && errorFingerprint === current.lastErrorFingerprint) {
|
|
362
|
+
current.repeatedErrorStreak += 1;
|
|
333
363
|
} else {
|
|
334
|
-
|
|
335
|
-
|
|
364
|
+
current.lastErrorFingerprint = errorFingerprint;
|
|
365
|
+
current.repeatedErrorStreak = errorFingerprint ? 1 : 0;
|
|
336
366
|
}
|
|
337
|
-
|
|
338
|
-
if (cfg.repeatedErrorBlockAfter > 0 &&
|
|
339
|
-
|
|
340
|
-
|
|
367
|
+
current.noDiffStreak = 0;
|
|
368
|
+
if (cfg.repeatedErrorBlockAfter > 0 && current.repeatedErrorStreak >= cfg.repeatedErrorBlockAfter && cfg.mode === "block") {
|
|
369
|
+
current.repeatedErrorBlocks += 1;
|
|
370
|
+
current.pendingBlockReason = `loop-breaker: the same tool error repeated ${current.repeatedErrorStreak} times. The next tool call is blocked so you can stop, summarize the repeated failure, and change approach.`;
|
|
341
371
|
}
|
|
342
|
-
if (
|
|
343
|
-
|
|
372
|
+
if (current.repeatedErrorStreak >= cfg.repeatedErrorWarnAfter) {
|
|
373
|
+
current.repeatedErrorWarnings += 1;
|
|
344
374
|
api.metrics.counter("repeated_error_warnings");
|
|
345
375
|
return {
|
|
346
376
|
decision: "allow",
|
|
347
|
-
additionalContext: `loop-breaker: same error repeated ${
|
|
377
|
+
additionalContext: `loop-breaker: same error repeated ${current.repeatedErrorStreak}x. Do not retry the same command/tool unchanged; inspect the root cause or ask for help.`
|
|
348
378
|
};
|
|
349
379
|
}
|
|
350
380
|
return;
|
|
351
381
|
}
|
|
352
|
-
|
|
353
|
-
|
|
382
|
+
current.lastErrorFingerprint = null;
|
|
383
|
+
current.repeatedErrorStreak = 0;
|
|
354
384
|
if (!MUTATING_TOOLS.has(toolName)) return;
|
|
355
385
|
const toolInput = input.toolInput ?? {};
|
|
356
386
|
const rawTarget = toolInput["path"] ?? toolInput["TargetFile"] ?? toolInput["filePath"] ?? toolInput["targetFile"] ?? toolInput["file_path"] ?? toolInput["destination"] ?? toolInput["file"];
|
|
@@ -362,22 +392,22 @@ var plugin = {
|
|
|
362
392
|
runtime.signal
|
|
363
393
|
);
|
|
364
394
|
if (diffFingerprint === null) return;
|
|
365
|
-
if (diffFingerprint ===
|
|
366
|
-
|
|
395
|
+
if (diffFingerprint === current.lastDiffFingerprint) {
|
|
396
|
+
current.noDiffStreak += 1;
|
|
367
397
|
} else {
|
|
368
|
-
|
|
369
|
-
|
|
398
|
+
current.lastDiffFingerprint = diffFingerprint;
|
|
399
|
+
current.noDiffStreak = 0;
|
|
370
400
|
}
|
|
371
|
-
if (cfg.noDiffBlockAfter > 0 &&
|
|
372
|
-
|
|
373
|
-
|
|
401
|
+
if (cfg.noDiffBlockAfter > 0 && current.noDiffStreak >= cfg.noDiffBlockAfter && cfg.mode === "block") {
|
|
402
|
+
current.noDiffBlocks += 1;
|
|
403
|
+
current.pendingBlockReason = `loop-breaker: no diff was produced in the last ${current.noDiffStreak} mutating step(s). The next tool call is blocked because continued edits are not changing the working tree.`;
|
|
374
404
|
}
|
|
375
|
-
if (
|
|
376
|
-
|
|
405
|
+
if (current.noDiffStreak >= cfg.noDiffWarnAfter) {
|
|
406
|
+
current.noDiffWarnings += 1;
|
|
377
407
|
api.metrics.counter("no_diff_warnings");
|
|
378
408
|
return {
|
|
379
409
|
decision: "allow",
|
|
380
|
-
additionalContext: `loop-breaker: no diff has changed for ${
|
|
410
|
+
additionalContext: `loop-breaker: no diff has changed for ${current.noDiffStreak} mutating step(s). Stop repeating edits; read the file/status, explain why no change is happening, or choose a different approach.`
|
|
381
411
|
};
|
|
382
412
|
}
|
|
383
413
|
return;
|
|
@@ -392,9 +422,18 @@ var plugin = {
|
|
|
392
422
|
timeoutMs: 2e3,
|
|
393
423
|
failurePolicy: "open"
|
|
394
424
|
});
|
|
425
|
+
const unregisterPrompt = api.registerHook(
|
|
426
|
+
"UserPromptSubmit",
|
|
427
|
+
void 0,
|
|
428
|
+
((input) => {
|
|
429
|
+
resetRun(input.sessionId);
|
|
430
|
+
}),
|
|
431
|
+
{ name: "loop-breaker-turn-reset", failurePolicy: "open" }
|
|
432
|
+
);
|
|
395
433
|
state.hookUnregister = () => {
|
|
396
434
|
unregisterPre();
|
|
397
435
|
unregisterPost();
|
|
436
|
+
unregisterPrompt();
|
|
398
437
|
};
|
|
399
438
|
api.tools.register({
|
|
400
439
|
name: "loop_breaker_status",
|
|
@@ -403,7 +442,8 @@ var plugin = {
|
|
|
403
442
|
permission: "auto",
|
|
404
443
|
category: "Diagnostics",
|
|
405
444
|
mutating: false,
|
|
406
|
-
async execute() {
|
|
445
|
+
async execute(_input, ctx) {
|
|
446
|
+
const current = runState(ctx?.session?.id);
|
|
407
447
|
return {
|
|
408
448
|
ok: true,
|
|
409
449
|
enabled: cfg.enabled,
|
|
@@ -417,20 +457,20 @@ var plugin = {
|
|
|
417
457
|
repeatedErrorWarnAfter: cfg.repeatedErrorWarnAfter,
|
|
418
458
|
repeatedErrorBlockAfter: cfg.repeatedErrorBlockAfter,
|
|
419
459
|
ignoreTools: cfg.ignoreTools,
|
|
420
|
-
currentStreak:
|
|
421
|
-
noDiffStreak:
|
|
422
|
-
repeatedErrorStreak:
|
|
460
|
+
currentStreak: current.streak,
|
|
461
|
+
noDiffStreak: current.noDiffStreak,
|
|
462
|
+
repeatedErrorStreak: current.repeatedErrorStreak,
|
|
423
463
|
counters: {
|
|
424
|
-
invocations:
|
|
425
|
-
postInvocations:
|
|
426
|
-
warnings:
|
|
427
|
-
blocks:
|
|
428
|
-
oscillationsDetected:
|
|
429
|
-
stepBudgetBlocks:
|
|
430
|
-
noDiffWarnings:
|
|
431
|
-
noDiffBlocks:
|
|
432
|
-
repeatedErrorWarnings:
|
|
433
|
-
repeatedErrorBlocks:
|
|
464
|
+
invocations: current.invocations,
|
|
465
|
+
postInvocations: current.postInvocations,
|
|
466
|
+
warnings: current.warnings,
|
|
467
|
+
blocks: current.blocks,
|
|
468
|
+
oscillationsDetected: current.oscillationsDetected,
|
|
469
|
+
stepBudgetBlocks: current.stepBudgetBlocks,
|
|
470
|
+
noDiffWarnings: current.noDiffWarnings,
|
|
471
|
+
noDiffBlocks: current.noDiffBlocks,
|
|
472
|
+
repeatedErrorWarnings: current.repeatedErrorWarnings,
|
|
473
|
+
repeatedErrorBlocks: current.repeatedErrorBlocks
|
|
434
474
|
}
|
|
435
475
|
};
|
|
436
476
|
}
|
|
@@ -451,53 +491,26 @@ var plugin = {
|
|
|
451
491
|
}
|
|
452
492
|
state.hookUnregister = null;
|
|
453
493
|
}
|
|
454
|
-
const final =
|
|
455
|
-
|
|
456
|
-
postInvocations: state.postInvocations,
|
|
457
|
-
warnings: state.warnings,
|
|
458
|
-
blocks: state.blocks,
|
|
459
|
-
oscillationsDetected: state.oscillationsDetected,
|
|
460
|
-
stepBudgetBlocks: state.stepBudgetBlocks,
|
|
461
|
-
noDiffWarnings: state.noDiffWarnings,
|
|
462
|
-
noDiffBlocks: state.noDiffBlocks,
|
|
463
|
-
repeatedErrorWarnings: state.repeatedErrorWarnings,
|
|
464
|
-
repeatedErrorBlocks: state.repeatedErrorBlocks
|
|
465
|
-
};
|
|
466
|
-
state.lastFingerprint = null;
|
|
467
|
-
state.streak = 0;
|
|
468
|
-
state.recent = [];
|
|
469
|
-
state.pendingBlockReason = null;
|
|
470
|
-
state.lastDiffFingerprint = null;
|
|
471
|
-
state.noDiffStreak = 0;
|
|
472
|
-
state.lastErrorFingerprint = null;
|
|
473
|
-
state.repeatedErrorStreak = 0;
|
|
474
|
-
state.invocations = 0;
|
|
475
|
-
state.postInvocations = 0;
|
|
476
|
-
state.warnings = 0;
|
|
477
|
-
state.blocks = 0;
|
|
478
|
-
state.oscillationsDetected = 0;
|
|
479
|
-
state.stepBudgetBlocks = 0;
|
|
480
|
-
state.noDiffWarnings = 0;
|
|
481
|
-
state.noDiffBlocks = 0;
|
|
482
|
-
state.repeatedErrorWarnings = 0;
|
|
483
|
-
state.repeatedErrorBlocks = 0;
|
|
494
|
+
const final = aggregateRuns();
|
|
495
|
+
state.runs.clear();
|
|
484
496
|
api.log.info("loop-breaker: teardown complete", { final });
|
|
485
497
|
},
|
|
486
498
|
async health() {
|
|
499
|
+
const totals = aggregateRuns();
|
|
487
500
|
return {
|
|
488
501
|
ok: true,
|
|
489
|
-
message: `loop-breaker: ${
|
|
502
|
+
message: `loop-breaker: ${totals.invocations} call(s) observed, ${totals.warnings + totals.noDiffWarnings + totals.repeatedErrorWarnings} warning(s), ${totals.blocks} block(s), ${totals.oscillationsDetected} oscillation(s)`,
|
|
490
503
|
counters: {
|
|
491
|
-
invocations:
|
|
492
|
-
postInvocations:
|
|
493
|
-
warnings:
|
|
494
|
-
blocks:
|
|
495
|
-
oscillationsDetected:
|
|
496
|
-
stepBudgetBlocks:
|
|
497
|
-
noDiffWarnings:
|
|
498
|
-
noDiffBlocks:
|
|
499
|
-
repeatedErrorWarnings:
|
|
500
|
-
repeatedErrorBlocks:
|
|
504
|
+
invocations: totals.invocations,
|
|
505
|
+
postInvocations: totals.postInvocations,
|
|
506
|
+
warnings: totals.warnings,
|
|
507
|
+
blocks: totals.blocks,
|
|
508
|
+
oscillationsDetected: totals.oscillationsDetected,
|
|
509
|
+
stepBudgetBlocks: totals.stepBudgetBlocks,
|
|
510
|
+
noDiffWarnings: totals.noDiffWarnings,
|
|
511
|
+
noDiffBlocks: totals.noDiffBlocks,
|
|
512
|
+
repeatedErrorWarnings: totals.repeatedErrorWarnings,
|
|
513
|
+
repeatedErrorBlocks: totals.repeatedErrorBlocks
|
|
501
514
|
}
|
|
502
515
|
};
|
|
503
516
|
}
|
package/dist/notify-hub.js
CHANGED
|
@@ -170,7 +170,8 @@ var state = {
|
|
|
170
170
|
lastDelivery: null,
|
|
171
171
|
stopHookUnregister: null,
|
|
172
172
|
eventUnsubscribers: [],
|
|
173
|
-
circuitWarned: false
|
|
173
|
+
circuitWarned: false,
|
|
174
|
+
disabledReason: null
|
|
174
175
|
};
|
|
175
176
|
var KNOWN_EVENTS = ["session.stop", "tool.error", "budget.threshold"];
|
|
176
177
|
var DEFAULTS = {
|
|
@@ -353,13 +354,21 @@ var plugin = {
|
|
|
353
354
|
}
|
|
354
355
|
}
|
|
355
356
|
state.eventUnsubscribers = [];
|
|
356
|
-
|
|
357
|
+
state.disabledReason = null;
|
|
358
|
+
const rawCfg = api.config.extensions?.["notify-hub"];
|
|
359
|
+
const cfg = readConfig(rawCfg);
|
|
357
360
|
let active = cfg.enabled && cfg.webhookUrl.length > 0;
|
|
361
|
+
const rawUrl = rawCfg?.["webhookUrl"] ?? rawCfg?.["webhook_url"] ?? rawCfg?.["url"];
|
|
362
|
+
if (cfg.enabled && typeof rawUrl === "string" && rawUrl.trim() && !cfg.webhookUrl) {
|
|
363
|
+
state.disabledReason = "webhookUrl was refused: it must be an http(s) URL without credentials that does not target localhost or a private address";
|
|
364
|
+
api.log.warn(`notify-hub: ${state.disabledReason}`);
|
|
365
|
+
}
|
|
358
366
|
if (active) {
|
|
359
367
|
try {
|
|
360
368
|
const url = new URL(cfg.webhookUrl);
|
|
361
369
|
const hostnameBlocked = await hasPrivateResolvedIP(url.hostname);
|
|
362
370
|
if (hostnameBlocked) {
|
|
371
|
+
state.disabledReason = `webhookUrl was refused: ${url.hostname} resolves to a private/local IP`;
|
|
363
372
|
api.log.warn(
|
|
364
373
|
`notify-hub: webhook URL resolves to a private/local IP (${url.hostname}) \u2014 disabling plugin`
|
|
365
374
|
);
|
|
@@ -432,7 +441,7 @@ var plugin = {
|
|
|
432
441
|
const ch = state.channel;
|
|
433
442
|
if (!ch) {
|
|
434
443
|
throw new Error(
|
|
435
|
-
'no webhookUrl configured \u2014 set config.extensions["notify-hub"].webhookUrl to enable deliveries'
|
|
444
|
+
state.disabledReason ?? 'no webhookUrl configured \u2014 set config.extensions["notify-hub"].webhookUrl to enable deliveries'
|
|
436
445
|
);
|
|
437
446
|
}
|
|
438
447
|
const inp = input ?? {};
|
|
@@ -35,7 +35,7 @@ var state = {
|
|
|
35
35
|
lastHookWarning: new runtime_exports.BoundedMap({ max: 512, ttlMs: HOOK_WARNING_COOLDOWN_MS })
|
|
36
36
|
};
|
|
37
37
|
var DEFAULTS = {
|
|
38
|
-
enabled:
|
|
38
|
+
enabled: true,
|
|
39
39
|
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
40
40
|
maxSuggestions: 5,
|
|
41
41
|
rules: { longFunctionLines: 50, maxParams: 5, maxNesting: 3 }
|
|
@@ -226,7 +226,7 @@ var plugin = {
|
|
|
226
226
|
configSchema: {
|
|
227
227
|
type: "object",
|
|
228
228
|
properties: {
|
|
229
|
-
enabled: { type: "boolean", default:
|
|
229
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
230
230
|
extensions: {
|
|
231
231
|
type: "array",
|
|
232
232
|
items: { type: "string" },
|
|
@@ -36,7 +36,7 @@ var state = {
|
|
|
36
36
|
hookUnregister: null
|
|
37
37
|
};
|
|
38
38
|
var DEFAULTS = {
|
|
39
|
-
enabled:
|
|
39
|
+
enabled: true,
|
|
40
40
|
severity: "warn",
|
|
41
41
|
maxFindings: 10,
|
|
42
42
|
scanOnChange: [
|
|
@@ -227,7 +227,7 @@ var plugin = {
|
|
|
227
227
|
configSchema: {
|
|
228
228
|
type: "object",
|
|
229
229
|
properties: {
|
|
230
|
-
enabled: { type: "boolean", default:
|
|
230
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
231
231
|
severity: {
|
|
232
232
|
type: "string",
|
|
233
233
|
enum: ["warn", "block"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/plugins",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "Official WrongStack collection of focused plugins for code quality, security, observability, planning, and agent coordination",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ECOSTACK TECHNOLOGY OÜ",
|
|
@@ -303,10 +303,10 @@
|
|
|
303
303
|
"vitest": "^5.0.0"
|
|
304
304
|
},
|
|
305
305
|
"dependencies": {
|
|
306
|
-
"@wrongstack/core": "1.0.
|
|
307
|
-
"@wrongstack/plugin-sdk": "1.0.
|
|
308
|
-
"@wrongstack/primitives": "1.0.
|
|
309
|
-
"@wrongstack/tools": "1.0.
|
|
306
|
+
"@wrongstack/core": "1.0.15",
|
|
307
|
+
"@wrongstack/plugin-sdk": "1.0.15",
|
|
308
|
+
"@wrongstack/primitives": "1.0.15",
|
|
309
|
+
"@wrongstack/tools": "1.0.15"
|
|
310
310
|
},
|
|
311
311
|
"scripts": {
|
|
312
312
|
"build": "node ../../scripts/build-package.mjs",
|