@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.
@@ -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 state = {
5
- lastFingerprint: null,
6
- streak: 0,
7
- recent: [],
8
- pendingBlockReason: null,
9
- lastDiffFingerprint: null,
10
- noDiffStreak: 0,
11
- lastErrorFingerprint: null,
12
- repeatedErrorStreak: 0,
13
- invocations: 0,
14
- postInvocations: 0,
15
- warnings: 0,
16
- blocks: 0,
17
- oscillationsDetected: 0,
18
- stepBudgetBlocks: 0,
19
- noDiffWarnings: 0,
20
- noDiffBlocks: 0,
21
- repeatedErrorWarnings: 0,
22
- repeatedErrorBlocks: 0,
23
- hookUnregister: null
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: true,
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: 200,
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"] !== false,
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: { type: "boolean", default: true, description: "Master switch." },
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: 200,
202
- description: "Maximum tool steps before blocking; 0 disables the step budget."
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.lastFingerprint = null;
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
- state.invocations += 1;
268
- if (state.pendingBlockReason && cfg.mode === "block") {
269
- const reason = state.pendingBlockReason;
270
- state.pendingBlockReason = null;
271
- state.blocks += 1;
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 && state.invocations > cfg.maxSteps && cfg.mode === "block") {
276
- state.blocks += 1;
277
- state.stepBudgetBlocks += 1;
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 (${state.invocations}/${cfg.maxSteps} tool calls). Stop and report what has been tried instead of continuing an unbounded loop.`
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 === state.lastFingerprint) {
287
- state.streak += 1;
315
+ if (fp === current.lastFingerprint) {
316
+ current.streak += 1;
288
317
  } else {
289
- state.lastFingerprint = fp;
290
- state.streak = 1;
318
+ current.lastFingerprint = fp;
319
+ current.streak = 1;
291
320
  }
292
- state.recent.push(fp);
293
- if (state.recent.length > Math.max(cfg.oscillationWindow, 16)) {
294
- state.recent.splice(0, state.recent.length - Math.max(cfg.oscillationWindow, 16));
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 (state.streak >= cfg.blockAfter && cfg.mode === "block") {
297
- state.blocks += 1;
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 ${state.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.)`
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 (state.streak >= cfg.warnAfter) {
305
- state.warnings += 1;
333
+ if (current.streak >= cfg.warnAfter) {
334
+ current.warnings += 1;
306
335
  api.metrics.counter("warnings");
307
- const remaining = cfg.mode === "block" ? cfg.blockAfter - state.streak : null;
336
+ const remaining = cfg.mode === "block" ? cfg.blockAfter - current.streak : null;
308
337
  return {
309
338
  decision: "allow",
310
- additionalContext: `loop-breaker: "${toolName}" repeated ${state.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."
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(state.recent, cfg.oscillationWindow)) {
314
- state.oscillationsDetected += 1;
342
+ if (isOscillating(current.recent, cfg.oscillationWindow)) {
343
+ current.oscillationsDetected += 1;
315
344
  api.metrics.counter("oscillations");
316
- state.recent = [];
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
- state.postInvocations += 1;
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 === state.lastErrorFingerprint) {
332
- state.repeatedErrorStreak += 1;
361
+ if (errorFingerprint && errorFingerprint === current.lastErrorFingerprint) {
362
+ current.repeatedErrorStreak += 1;
333
363
  } else {
334
- state.lastErrorFingerprint = errorFingerprint;
335
- state.repeatedErrorStreak = errorFingerprint ? 1 : 0;
364
+ current.lastErrorFingerprint = errorFingerprint;
365
+ current.repeatedErrorStreak = errorFingerprint ? 1 : 0;
336
366
  }
337
- state.noDiffStreak = 0;
338
- if (cfg.repeatedErrorBlockAfter > 0 && state.repeatedErrorStreak >= cfg.repeatedErrorBlockAfter && cfg.mode === "block") {
339
- state.repeatedErrorBlocks += 1;
340
- state.pendingBlockReason = `loop-breaker: the same tool error repeated ${state.repeatedErrorStreak} times. The next tool call is blocked so you can stop, summarize the repeated failure, and change approach.`;
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 (state.repeatedErrorStreak >= cfg.repeatedErrorWarnAfter) {
343
- state.repeatedErrorWarnings += 1;
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 ${state.repeatedErrorStreak}x. Do not retry the same command/tool unchanged; inspect the root cause or ask for help.`
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
- state.lastErrorFingerprint = null;
353
- state.repeatedErrorStreak = 0;
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 === state.lastDiffFingerprint) {
366
- state.noDiffStreak += 1;
395
+ if (diffFingerprint === current.lastDiffFingerprint) {
396
+ current.noDiffStreak += 1;
367
397
  } else {
368
- state.lastDiffFingerprint = diffFingerprint;
369
- state.noDiffStreak = 0;
398
+ current.lastDiffFingerprint = diffFingerprint;
399
+ current.noDiffStreak = 0;
370
400
  }
371
- if (cfg.noDiffBlockAfter > 0 && state.noDiffStreak >= cfg.noDiffBlockAfter && cfg.mode === "block") {
372
- state.noDiffBlocks += 1;
373
- state.pendingBlockReason = `loop-breaker: no diff was produced in the last ${state.noDiffStreak} mutating step(s). The next tool call is blocked because continued edits are not changing the working tree.`;
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 (state.noDiffStreak >= cfg.noDiffWarnAfter) {
376
- state.noDiffWarnings += 1;
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 ${state.noDiffStreak} mutating step(s). Stop repeating edits; read the file/status, explain why no change is happening, or choose a different approach.`
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: state.streak,
421
- noDiffStreak: state.noDiffStreak,
422
- repeatedErrorStreak: state.repeatedErrorStreak,
460
+ currentStreak: current.streak,
461
+ noDiffStreak: current.noDiffStreak,
462
+ repeatedErrorStreak: current.repeatedErrorStreak,
423
463
  counters: {
424
- invocations: state.invocations,
425
- postInvocations: state.postInvocations,
426
- warnings: state.warnings,
427
- blocks: state.blocks,
428
- oscillationsDetected: state.oscillationsDetected,
429
- stepBudgetBlocks: state.stepBudgetBlocks,
430
- noDiffWarnings: state.noDiffWarnings,
431
- noDiffBlocks: state.noDiffBlocks,
432
- repeatedErrorWarnings: state.repeatedErrorWarnings,
433
- repeatedErrorBlocks: state.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
- invocations: state.invocations,
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: ${state.invocations} call(s) observed, ${state.warnings + state.noDiffWarnings + state.repeatedErrorWarnings} warning(s), ${state.blocks} block(s), ${state.oscillationsDetected} oscillation(s)`,
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: state.invocations,
492
- postInvocations: state.postInvocations,
493
- warnings: state.warnings,
494
- blocks: state.blocks,
495
- oscillationsDetected: state.oscillationsDetected,
496
- stepBudgetBlocks: state.stepBudgetBlocks,
497
- noDiffWarnings: state.noDiffWarnings,
498
- noDiffBlocks: state.noDiffBlocks,
499
- repeatedErrorWarnings: state.repeatedErrorWarnings,
500
- repeatedErrorBlocks: state.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
  }
@@ -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
- const cfg = readConfig(api.config.extensions?.["notify-hub"]);
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: false,
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: false, description: "Master switch." },
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: false,
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: false, description: "Master switch." },
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.13",
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.13",
307
- "@wrongstack/plugin-sdk": "1.0.13",
308
- "@wrongstack/primitives": "1.0.13",
309
- "@wrongstack/tools": "1.0.13"
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",