@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/index.js
CHANGED
|
@@ -33,7 +33,7 @@ var state = {
|
|
|
33
33
|
hookUnregister: null
|
|
34
34
|
};
|
|
35
35
|
var DEFAULTS = {
|
|
36
|
-
enabled:
|
|
36
|
+
enabled: true,
|
|
37
37
|
includeExtensions: [".tsx", ".jsx", ".html", ".vue"],
|
|
38
38
|
maxFindings: 50,
|
|
39
39
|
severity: "warn",
|
|
@@ -1522,6 +1522,10 @@ var plugin4 = {
|
|
|
1522
1522
|
},
|
|
1523
1523
|
permission: "auto",
|
|
1524
1524
|
mutating: true,
|
|
1525
|
+
// It rewrites project source files, so it declares the same capability
|
|
1526
|
+
// as the built-in writers: the agent-state write gate and capability
|
|
1527
|
+
// allowlists key on `fs.write`, not on `mutating`.
|
|
1528
|
+
capabilities: ["fs.write"],
|
|
1525
1529
|
category: "Project",
|
|
1526
1530
|
async execute(input) {
|
|
1527
1531
|
const inp = input;
|
|
@@ -1773,7 +1777,7 @@ var state6 = {
|
|
|
1773
1777
|
lastResult: null
|
|
1774
1778
|
};
|
|
1775
1779
|
var DEFAULTS5 = {
|
|
1776
|
-
enabled:
|
|
1780
|
+
enabled: true,
|
|
1777
1781
|
fileExtensions: [".tsx", ".jsx", ".vue"],
|
|
1778
1782
|
minLength: 2,
|
|
1779
1783
|
maxContextStrings: 10,
|
|
@@ -3317,7 +3321,7 @@ var state10 = {
|
|
|
3317
3321
|
hookUnregister: null
|
|
3318
3322
|
};
|
|
3319
3323
|
var DEFAULTS9 = {
|
|
3320
|
-
enabled:
|
|
3324
|
+
enabled: true,
|
|
3321
3325
|
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
3322
3326
|
maxFiles: 50
|
|
3323
3327
|
};
|
|
@@ -5088,6 +5092,7 @@ var cost_tracker_default = plugin14;
|
|
|
5088
5092
|
import { ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
|
|
5089
5093
|
var COORDINATION_CRON_CAPABILITY = "coordination.cron";
|
|
5090
5094
|
var API_VERSION9 = "^0.1.10";
|
|
5095
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
5091
5096
|
var state14 = {
|
|
5092
5097
|
jobs: /* @__PURE__ */ new Map(),
|
|
5093
5098
|
timers: /* @__PURE__ */ new Map(),
|
|
@@ -5233,7 +5238,9 @@ var plugin15 = {
|
|
|
5233
5238
|
name: { type: "string", description: "Unique name for this cron job" },
|
|
5234
5239
|
intervalMs: {
|
|
5235
5240
|
type: "number",
|
|
5236
|
-
|
|
5241
|
+
minimum: 1e3,
|
|
5242
|
+
maximum: MAX_TIMER_DELAY_MS,
|
|
5243
|
+
description: `Interval between runs in milliseconds (1000 to ${MAX_TIMER_DELAY_MS})`
|
|
5237
5244
|
},
|
|
5238
5245
|
action: {
|
|
5239
5246
|
type: "string",
|
|
@@ -5265,6 +5272,12 @@ var plugin15 = {
|
|
|
5265
5272
|
field: "intervalMs"
|
|
5266
5273
|
});
|
|
5267
5274
|
}
|
|
5275
|
+
if (intervalMs > MAX_TIMER_DELAY_MS) {
|
|
5276
|
+
throw new ToolValidationError8({
|
|
5277
|
+
message: `intervalMs must be <= ${MAX_TIMER_DELAY_MS} (about 24.8 days)`,
|
|
5278
|
+
field: "intervalMs"
|
|
5279
|
+
});
|
|
5280
|
+
}
|
|
5268
5281
|
if (typeof action !== "string" || action.trim() === "") {
|
|
5269
5282
|
throw new ToolValidationError8({
|
|
5270
5283
|
message: "action is required and must be a non-empty string",
|
|
@@ -5390,7 +5403,7 @@ var state15 = {
|
|
|
5390
5403
|
hookUnregister: null
|
|
5391
5404
|
};
|
|
5392
5405
|
var DEFAULTS13 = {
|
|
5393
|
-
enabled:
|
|
5406
|
+
enabled: true,
|
|
5394
5407
|
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
5395
5408
|
defaultDepth: 3,
|
|
5396
5409
|
maxDepth: 10,
|
|
@@ -5546,7 +5559,7 @@ var plugin16 = {
|
|
|
5546
5559
|
properties: {
|
|
5547
5560
|
enabled: {
|
|
5548
5561
|
type: "boolean",
|
|
5549
|
-
default:
|
|
5562
|
+
default: true,
|
|
5550
5563
|
description: "Master switch."
|
|
5551
5564
|
},
|
|
5552
5565
|
extensions: {
|
|
@@ -7103,7 +7116,7 @@ var hookIndexBudgets = {
|
|
|
7103
7116
|
maxFileBytes: 2 * 1024 * 1024
|
|
7104
7117
|
};
|
|
7105
7118
|
var DEFAULTS18 = {
|
|
7106
|
-
enabled:
|
|
7119
|
+
enabled: true,
|
|
7107
7120
|
minLines: 8,
|
|
7108
7121
|
threshold: 0.8,
|
|
7109
7122
|
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
@@ -7319,7 +7332,7 @@ var plugin21 = {
|
|
|
7319
7332
|
configSchema: {
|
|
7320
7333
|
type: "object",
|
|
7321
7334
|
properties: {
|
|
7322
|
-
enabled: { type: "boolean", default:
|
|
7335
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
7323
7336
|
minLines: {
|
|
7324
7337
|
type: "number",
|
|
7325
7338
|
minimum: 2,
|
|
@@ -7915,7 +7928,7 @@ var DEFAULT_PATTERNS2 = [
|
|
|
7915
7928
|
String.raw`flags(?:\.|\?\.)([A-Za-z_$][A-Za-z0-9_$]*)`
|
|
7916
7929
|
];
|
|
7917
7930
|
var DEFAULTS20 = {
|
|
7918
|
-
enabled:
|
|
7931
|
+
enabled: true,
|
|
7919
7932
|
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
7920
7933
|
patterns: [],
|
|
7921
7934
|
maxFindings: 50
|
|
@@ -9430,8 +9443,9 @@ var plugin26 = {
|
|
|
9430
9443
|
try {
|
|
9431
9444
|
commitScope = await stageFiles(files);
|
|
9432
9445
|
} catch (err) {
|
|
9446
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
9433
9447
|
throw new Error(
|
|
9434
|
-
|
|
9448
|
+
detail.startsWith("Failed to stage files") ? detail : `Failed to stage files: ${detail}`,
|
|
9435
9449
|
{ cause: err }
|
|
9436
9450
|
);
|
|
9437
9451
|
}
|
|
@@ -10747,7 +10761,7 @@ var state27 = {
|
|
|
10747
10761
|
hookUnregister: null
|
|
10748
10762
|
};
|
|
10749
10763
|
var DEFAULTS25 = {
|
|
10750
|
-
enabled:
|
|
10764
|
+
enabled: true,
|
|
10751
10765
|
extensions: [".ts", ".tsx"],
|
|
10752
10766
|
maxFindings: 50,
|
|
10753
10767
|
maxFiles: 2e3
|
|
@@ -12465,29 +12479,70 @@ var llm_cache_default = plugin34;
|
|
|
12465
12479
|
// src/loop-breaker/index.ts
|
|
12466
12480
|
import { execFile as execFile8 } from "node:child_process";
|
|
12467
12481
|
import { isAbsolute as isAbsolute15, relative as relative15 } from "node:path";
|
|
12468
|
-
var
|
|
12469
|
-
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12482
|
+
var FALLBACK_SESSION = "__default__";
|
|
12483
|
+
var MAX_TRACKED_RUNS = 32;
|
|
12484
|
+
function createRunState() {
|
|
12485
|
+
return {
|
|
12486
|
+
lastFingerprint: null,
|
|
12487
|
+
streak: 0,
|
|
12488
|
+
recent: [],
|
|
12489
|
+
pendingBlockReason: null,
|
|
12490
|
+
lastDiffFingerprint: null,
|
|
12491
|
+
noDiffStreak: 0,
|
|
12492
|
+
lastErrorFingerprint: null,
|
|
12493
|
+
repeatedErrorStreak: 0,
|
|
12494
|
+
invocations: 0,
|
|
12495
|
+
postInvocations: 0,
|
|
12496
|
+
warnings: 0,
|
|
12497
|
+
blocks: 0,
|
|
12498
|
+
oscillationsDetected: 0,
|
|
12499
|
+
stepBudgetBlocks: 0,
|
|
12500
|
+
noDiffWarnings: 0,
|
|
12501
|
+
noDiffBlocks: 0,
|
|
12502
|
+
repeatedErrorWarnings: 0,
|
|
12503
|
+
repeatedErrorBlocks: 0
|
|
12504
|
+
};
|
|
12505
|
+
}
|
|
12506
|
+
var state32 = { runs: /* @__PURE__ */ new Map(), hookUnregister: null };
|
|
12507
|
+
function sessionKey(sessionId) {
|
|
12508
|
+
return sessionId || FALLBACK_SESSION;
|
|
12509
|
+
}
|
|
12510
|
+
function storeRun(key, current) {
|
|
12511
|
+
state32.runs.set(key, current);
|
|
12512
|
+
if (state32.runs.size <= MAX_TRACKED_RUNS) return;
|
|
12513
|
+
const oldest = state32.runs.keys().next().value;
|
|
12514
|
+
if (oldest !== void 0 && oldest !== key) state32.runs.delete(oldest);
|
|
12515
|
+
}
|
|
12516
|
+
function runState(sessionId) {
|
|
12517
|
+
const key = sessionKey(sessionId);
|
|
12518
|
+
let current = state32.runs.get(key);
|
|
12519
|
+
if (!current) {
|
|
12520
|
+
current = createRunState();
|
|
12521
|
+
storeRun(key, current);
|
|
12522
|
+
}
|
|
12523
|
+
return current;
|
|
12524
|
+
}
|
|
12525
|
+
function resetRun(sessionId) {
|
|
12526
|
+
storeRun(sessionKey(sessionId), createRunState());
|
|
12527
|
+
}
|
|
12528
|
+
function aggregateRuns() {
|
|
12529
|
+
const total = createRunState();
|
|
12530
|
+
for (const current of state32.runs.values()) {
|
|
12531
|
+
total.invocations += current.invocations;
|
|
12532
|
+
total.postInvocations += current.postInvocations;
|
|
12533
|
+
total.warnings += current.warnings;
|
|
12534
|
+
total.blocks += current.blocks;
|
|
12535
|
+
total.oscillationsDetected += current.oscillationsDetected;
|
|
12536
|
+
total.stepBudgetBlocks += current.stepBudgetBlocks;
|
|
12537
|
+
total.noDiffWarnings += current.noDiffWarnings;
|
|
12538
|
+
total.noDiffBlocks += current.noDiffBlocks;
|
|
12539
|
+
total.repeatedErrorWarnings += current.repeatedErrorWarnings;
|
|
12540
|
+
total.repeatedErrorBlocks += current.repeatedErrorBlocks;
|
|
12541
|
+
}
|
|
12542
|
+
return total;
|
|
12543
|
+
}
|
|
12489
12544
|
var DEFAULTS30 = {
|
|
12490
|
-
enabled:
|
|
12545
|
+
enabled: false,
|
|
12491
12546
|
// Documented contract (feature matrix, plugin description): warn, then
|
|
12492
12547
|
// block. A warn-only default meant an agent stuck re-issuing the same call
|
|
12493
12548
|
// was never actually stopped unless the user happened to set any option.
|
|
@@ -12495,7 +12550,7 @@ var DEFAULTS30 = {
|
|
|
12495
12550
|
warnAfter: 3,
|
|
12496
12551
|
blockAfter: 5,
|
|
12497
12552
|
oscillationWindow: 8,
|
|
12498
|
-
maxSteps:
|
|
12553
|
+
maxSteps: 0,
|
|
12499
12554
|
noDiffWarnAfter: 6,
|
|
12500
12555
|
noDiffBlockAfter: 10,
|
|
12501
12556
|
repeatedErrorWarnAfter: 2,
|
|
@@ -12521,7 +12576,7 @@ function readConfig30(raw) {
|
|
|
12521
12576
|
const rawOsc = r["oscillationWindow"] ?? r["oscillation_window"] ?? r["window"];
|
|
12522
12577
|
const rawMaxSteps = r["maxSteps"] ?? r["max_steps"] ?? r["stepLimit"] ?? r["step_limit"];
|
|
12523
12578
|
return {
|
|
12524
|
-
enabled: r["enabled"]
|
|
12579
|
+
enabled: r["enabled"] === true,
|
|
12525
12580
|
mode,
|
|
12526
12581
|
warnAfter,
|
|
12527
12582
|
blockAfter,
|
|
@@ -12634,7 +12689,11 @@ var plugin35 = {
|
|
|
12634
12689
|
configSchema: {
|
|
12635
12690
|
type: "object",
|
|
12636
12691
|
properties: {
|
|
12637
|
-
enabled: {
|
|
12692
|
+
enabled: {
|
|
12693
|
+
type: "boolean",
|
|
12694
|
+
default: false,
|
|
12695
|
+
description: "Opt-in master switch; disabled unless explicitly enabled."
|
|
12696
|
+
},
|
|
12638
12697
|
mode: {
|
|
12639
12698
|
type: "string",
|
|
12640
12699
|
enum: ["warn", "block"],
|
|
@@ -12662,8 +12721,8 @@ var plugin35 = {
|
|
|
12662
12721
|
maxSteps: {
|
|
12663
12722
|
type: "number",
|
|
12664
12723
|
minimum: 0,
|
|
12665
|
-
default:
|
|
12666
|
-
description: "
|
|
12724
|
+
default: 0,
|
|
12725
|
+
description: "Optional maximum tool steps before blocking; 0 keeps runs unlimited."
|
|
12667
12726
|
},
|
|
12668
12727
|
noDiffWarnAfter: {
|
|
12669
12728
|
type: "number",
|
|
@@ -12698,24 +12757,7 @@ var plugin35 = {
|
|
|
12698
12757
|
}
|
|
12699
12758
|
},
|
|
12700
12759
|
setup(api) {
|
|
12701
|
-
state32.
|
|
12702
|
-
state32.streak = 0;
|
|
12703
|
-
state32.recent = [];
|
|
12704
|
-
state32.pendingBlockReason = null;
|
|
12705
|
-
state32.lastDiffFingerprint = null;
|
|
12706
|
-
state32.noDiffStreak = 0;
|
|
12707
|
-
state32.lastErrorFingerprint = null;
|
|
12708
|
-
state32.repeatedErrorStreak = 0;
|
|
12709
|
-
state32.invocations = 0;
|
|
12710
|
-
state32.postInvocations = 0;
|
|
12711
|
-
state32.warnings = 0;
|
|
12712
|
-
state32.blocks = 0;
|
|
12713
|
-
state32.oscillationsDetected = 0;
|
|
12714
|
-
state32.stepBudgetBlocks = 0;
|
|
12715
|
-
state32.noDiffWarnings = 0;
|
|
12716
|
-
state32.noDiffBlocks = 0;
|
|
12717
|
-
state32.repeatedErrorWarnings = 0;
|
|
12718
|
-
state32.repeatedErrorBlocks = 0;
|
|
12760
|
+
state32.runs.clear();
|
|
12719
12761
|
if (state32.hookUnregister) {
|
|
12720
12762
|
try {
|
|
12721
12763
|
state32.hookUnregister();
|
|
@@ -12728,56 +12770,57 @@ var plugin35 = {
|
|
|
12728
12770
|
if (!cfg.enabled) return;
|
|
12729
12771
|
const toolName = input.toolName ?? "unknown";
|
|
12730
12772
|
if (ignoreToolsSet.has(toolName)) return;
|
|
12731
|
-
|
|
12732
|
-
if (
|
|
12733
|
-
const reason =
|
|
12734
|
-
|
|
12735
|
-
|
|
12773
|
+
const current = runState(input.sessionId);
|
|
12774
|
+
if (current.pendingBlockReason && cfg.mode === "block") {
|
|
12775
|
+
const reason = current.pendingBlockReason;
|
|
12776
|
+
current.pendingBlockReason = null;
|
|
12777
|
+
current.blocks += 1;
|
|
12736
12778
|
api.metrics.counter("blocks");
|
|
12737
12779
|
return { decision: "block", reason };
|
|
12738
12780
|
}
|
|
12739
|
-
if (cfg.maxSteps > 0 &&
|
|
12740
|
-
|
|
12741
|
-
|
|
12781
|
+
if (cfg.maxSteps > 0 && current.invocations >= cfg.maxSteps && cfg.mode === "block") {
|
|
12782
|
+
current.blocks += 1;
|
|
12783
|
+
current.stepBudgetBlocks += 1;
|
|
12742
12784
|
api.metrics.counter("blocks");
|
|
12743
12785
|
api.metrics.counter("step_budget_blocks");
|
|
12744
12786
|
return {
|
|
12745
12787
|
decision: "block",
|
|
12746
|
-
reason: `loop-breaker: step budget exceeded (${
|
|
12788
|
+
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.`
|
|
12747
12789
|
};
|
|
12748
12790
|
}
|
|
12791
|
+
current.invocations += 1;
|
|
12749
12792
|
const fp = fingerprint(toolName, input.toolInput);
|
|
12750
|
-
if (fp ===
|
|
12751
|
-
|
|
12793
|
+
if (fp === current.lastFingerprint) {
|
|
12794
|
+
current.streak += 1;
|
|
12752
12795
|
} else {
|
|
12753
|
-
|
|
12754
|
-
|
|
12796
|
+
current.lastFingerprint = fp;
|
|
12797
|
+
current.streak = 1;
|
|
12755
12798
|
}
|
|
12756
|
-
|
|
12757
|
-
if (
|
|
12758
|
-
|
|
12799
|
+
current.recent.push(fp);
|
|
12800
|
+
if (current.recent.length > Math.max(cfg.oscillationWindow, 16)) {
|
|
12801
|
+
current.recent.splice(0, current.recent.length - Math.max(cfg.oscillationWindow, 16));
|
|
12759
12802
|
}
|
|
12760
|
-
if (
|
|
12761
|
-
|
|
12803
|
+
if (current.streak >= cfg.blockAfter && cfg.mode === "block") {
|
|
12804
|
+
current.blocks += 1;
|
|
12762
12805
|
api.metrics.counter("blocks");
|
|
12763
12806
|
return {
|
|
12764
12807
|
decision: "block",
|
|
12765
|
-
reason: `loop-breaker: "${toolName}" has been called ${
|
|
12808
|
+
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.)`
|
|
12766
12809
|
};
|
|
12767
12810
|
}
|
|
12768
|
-
if (
|
|
12769
|
-
|
|
12811
|
+
if (current.streak >= cfg.warnAfter) {
|
|
12812
|
+
current.warnings += 1;
|
|
12770
12813
|
api.metrics.counter("warnings");
|
|
12771
|
-
const remaining = cfg.mode === "block" ? cfg.blockAfter -
|
|
12814
|
+
const remaining = cfg.mode === "block" ? cfg.blockAfter - current.streak : null;
|
|
12772
12815
|
return {
|
|
12773
12816
|
decision: "allow",
|
|
12774
|
-
additionalContext: `loop-breaker: "${toolName}" repeated ${
|
|
12817
|
+
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."
|
|
12775
12818
|
};
|
|
12776
12819
|
}
|
|
12777
|
-
if (isOscillating(
|
|
12778
|
-
|
|
12820
|
+
if (isOscillating(current.recent, cfg.oscillationWindow)) {
|
|
12821
|
+
current.oscillationsDetected += 1;
|
|
12779
12822
|
api.metrics.counter("oscillations");
|
|
12780
|
-
|
|
12823
|
+
current.recent = [];
|
|
12781
12824
|
return {
|
|
12782
12825
|
decision: "allow",
|
|
12783
12826
|
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.`
|
|
@@ -12789,32 +12832,33 @@ var plugin35 = {
|
|
|
12789
12832
|
if (!cfg.enabled) return;
|
|
12790
12833
|
const toolName = input.toolName ?? "unknown";
|
|
12791
12834
|
if (ignoreToolsSet.has(toolName)) return;
|
|
12792
|
-
|
|
12835
|
+
const current = runState(input.sessionId);
|
|
12836
|
+
current.postInvocations += 1;
|
|
12793
12837
|
if (input.toolResult?.isError) {
|
|
12794
12838
|
const errorFingerprint = normalizeError(input.toolResult.content);
|
|
12795
|
-
if (errorFingerprint && errorFingerprint ===
|
|
12796
|
-
|
|
12839
|
+
if (errorFingerprint && errorFingerprint === current.lastErrorFingerprint) {
|
|
12840
|
+
current.repeatedErrorStreak += 1;
|
|
12797
12841
|
} else {
|
|
12798
|
-
|
|
12799
|
-
|
|
12842
|
+
current.lastErrorFingerprint = errorFingerprint;
|
|
12843
|
+
current.repeatedErrorStreak = errorFingerprint ? 1 : 0;
|
|
12800
12844
|
}
|
|
12801
|
-
|
|
12802
|
-
if (cfg.repeatedErrorBlockAfter > 0 &&
|
|
12803
|
-
|
|
12804
|
-
|
|
12845
|
+
current.noDiffStreak = 0;
|
|
12846
|
+
if (cfg.repeatedErrorBlockAfter > 0 && current.repeatedErrorStreak >= cfg.repeatedErrorBlockAfter && cfg.mode === "block") {
|
|
12847
|
+
current.repeatedErrorBlocks += 1;
|
|
12848
|
+
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.`;
|
|
12805
12849
|
}
|
|
12806
|
-
if (
|
|
12807
|
-
|
|
12850
|
+
if (current.repeatedErrorStreak >= cfg.repeatedErrorWarnAfter) {
|
|
12851
|
+
current.repeatedErrorWarnings += 1;
|
|
12808
12852
|
api.metrics.counter("repeated_error_warnings");
|
|
12809
12853
|
return {
|
|
12810
12854
|
decision: "allow",
|
|
12811
|
-
additionalContext: `loop-breaker: same error repeated ${
|
|
12855
|
+
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.`
|
|
12812
12856
|
};
|
|
12813
12857
|
}
|
|
12814
12858
|
return;
|
|
12815
12859
|
}
|
|
12816
|
-
|
|
12817
|
-
|
|
12860
|
+
current.lastErrorFingerprint = null;
|
|
12861
|
+
current.repeatedErrorStreak = 0;
|
|
12818
12862
|
if (!MUTATING_TOOLS.has(toolName)) return;
|
|
12819
12863
|
const toolInput = input.toolInput ?? {};
|
|
12820
12864
|
const rawTarget = toolInput["path"] ?? toolInput["TargetFile"] ?? toolInput["filePath"] ?? toolInput["targetFile"] ?? toolInput["file_path"] ?? toolInput["destination"] ?? toolInput["file"];
|
|
@@ -12826,22 +12870,22 @@ var plugin35 = {
|
|
|
12826
12870
|
runtime.signal
|
|
12827
12871
|
);
|
|
12828
12872
|
if (diffFingerprint === null) return;
|
|
12829
|
-
if (diffFingerprint ===
|
|
12830
|
-
|
|
12873
|
+
if (diffFingerprint === current.lastDiffFingerprint) {
|
|
12874
|
+
current.noDiffStreak += 1;
|
|
12831
12875
|
} else {
|
|
12832
|
-
|
|
12833
|
-
|
|
12876
|
+
current.lastDiffFingerprint = diffFingerprint;
|
|
12877
|
+
current.noDiffStreak = 0;
|
|
12834
12878
|
}
|
|
12835
|
-
if (cfg.noDiffBlockAfter > 0 &&
|
|
12836
|
-
|
|
12837
|
-
|
|
12879
|
+
if (cfg.noDiffBlockAfter > 0 && current.noDiffStreak >= cfg.noDiffBlockAfter && cfg.mode === "block") {
|
|
12880
|
+
current.noDiffBlocks += 1;
|
|
12881
|
+
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.`;
|
|
12838
12882
|
}
|
|
12839
|
-
if (
|
|
12840
|
-
|
|
12883
|
+
if (current.noDiffStreak >= cfg.noDiffWarnAfter) {
|
|
12884
|
+
current.noDiffWarnings += 1;
|
|
12841
12885
|
api.metrics.counter("no_diff_warnings");
|
|
12842
12886
|
return {
|
|
12843
12887
|
decision: "allow",
|
|
12844
|
-
additionalContext: `loop-breaker: no diff has changed for ${
|
|
12888
|
+
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.`
|
|
12845
12889
|
};
|
|
12846
12890
|
}
|
|
12847
12891
|
return;
|
|
@@ -12856,9 +12900,18 @@ var plugin35 = {
|
|
|
12856
12900
|
timeoutMs: 2e3,
|
|
12857
12901
|
failurePolicy: "open"
|
|
12858
12902
|
});
|
|
12903
|
+
const unregisterPrompt = api.registerHook(
|
|
12904
|
+
"UserPromptSubmit",
|
|
12905
|
+
void 0,
|
|
12906
|
+
((input) => {
|
|
12907
|
+
resetRun(input.sessionId);
|
|
12908
|
+
}),
|
|
12909
|
+
{ name: "loop-breaker-turn-reset", failurePolicy: "open" }
|
|
12910
|
+
);
|
|
12859
12911
|
state32.hookUnregister = () => {
|
|
12860
12912
|
unregisterPre();
|
|
12861
12913
|
unregisterPost();
|
|
12914
|
+
unregisterPrompt();
|
|
12862
12915
|
};
|
|
12863
12916
|
api.tools.register({
|
|
12864
12917
|
name: "loop_breaker_status",
|
|
@@ -12867,7 +12920,8 @@ var plugin35 = {
|
|
|
12867
12920
|
permission: "auto",
|
|
12868
12921
|
category: "Diagnostics",
|
|
12869
12922
|
mutating: false,
|
|
12870
|
-
async execute() {
|
|
12923
|
+
async execute(_input, ctx) {
|
|
12924
|
+
const current = runState(ctx?.session?.id);
|
|
12871
12925
|
return {
|
|
12872
12926
|
ok: true,
|
|
12873
12927
|
enabled: cfg.enabled,
|
|
@@ -12881,20 +12935,20 @@ var plugin35 = {
|
|
|
12881
12935
|
repeatedErrorWarnAfter: cfg.repeatedErrorWarnAfter,
|
|
12882
12936
|
repeatedErrorBlockAfter: cfg.repeatedErrorBlockAfter,
|
|
12883
12937
|
ignoreTools: cfg.ignoreTools,
|
|
12884
|
-
currentStreak:
|
|
12885
|
-
noDiffStreak:
|
|
12886
|
-
repeatedErrorStreak:
|
|
12938
|
+
currentStreak: current.streak,
|
|
12939
|
+
noDiffStreak: current.noDiffStreak,
|
|
12940
|
+
repeatedErrorStreak: current.repeatedErrorStreak,
|
|
12887
12941
|
counters: {
|
|
12888
|
-
invocations:
|
|
12889
|
-
postInvocations:
|
|
12890
|
-
warnings:
|
|
12891
|
-
blocks:
|
|
12892
|
-
oscillationsDetected:
|
|
12893
|
-
stepBudgetBlocks:
|
|
12894
|
-
noDiffWarnings:
|
|
12895
|
-
noDiffBlocks:
|
|
12896
|
-
repeatedErrorWarnings:
|
|
12897
|
-
repeatedErrorBlocks:
|
|
12942
|
+
invocations: current.invocations,
|
|
12943
|
+
postInvocations: current.postInvocations,
|
|
12944
|
+
warnings: current.warnings,
|
|
12945
|
+
blocks: current.blocks,
|
|
12946
|
+
oscillationsDetected: current.oscillationsDetected,
|
|
12947
|
+
stepBudgetBlocks: current.stepBudgetBlocks,
|
|
12948
|
+
noDiffWarnings: current.noDiffWarnings,
|
|
12949
|
+
noDiffBlocks: current.noDiffBlocks,
|
|
12950
|
+
repeatedErrorWarnings: current.repeatedErrorWarnings,
|
|
12951
|
+
repeatedErrorBlocks: current.repeatedErrorBlocks
|
|
12898
12952
|
}
|
|
12899
12953
|
};
|
|
12900
12954
|
}
|
|
@@ -12915,53 +12969,26 @@ var plugin35 = {
|
|
|
12915
12969
|
}
|
|
12916
12970
|
state32.hookUnregister = null;
|
|
12917
12971
|
}
|
|
12918
|
-
const final =
|
|
12919
|
-
|
|
12920
|
-
postInvocations: state32.postInvocations,
|
|
12921
|
-
warnings: state32.warnings,
|
|
12922
|
-
blocks: state32.blocks,
|
|
12923
|
-
oscillationsDetected: state32.oscillationsDetected,
|
|
12924
|
-
stepBudgetBlocks: state32.stepBudgetBlocks,
|
|
12925
|
-
noDiffWarnings: state32.noDiffWarnings,
|
|
12926
|
-
noDiffBlocks: state32.noDiffBlocks,
|
|
12927
|
-
repeatedErrorWarnings: state32.repeatedErrorWarnings,
|
|
12928
|
-
repeatedErrorBlocks: state32.repeatedErrorBlocks
|
|
12929
|
-
};
|
|
12930
|
-
state32.lastFingerprint = null;
|
|
12931
|
-
state32.streak = 0;
|
|
12932
|
-
state32.recent = [];
|
|
12933
|
-
state32.pendingBlockReason = null;
|
|
12934
|
-
state32.lastDiffFingerprint = null;
|
|
12935
|
-
state32.noDiffStreak = 0;
|
|
12936
|
-
state32.lastErrorFingerprint = null;
|
|
12937
|
-
state32.repeatedErrorStreak = 0;
|
|
12938
|
-
state32.invocations = 0;
|
|
12939
|
-
state32.postInvocations = 0;
|
|
12940
|
-
state32.warnings = 0;
|
|
12941
|
-
state32.blocks = 0;
|
|
12942
|
-
state32.oscillationsDetected = 0;
|
|
12943
|
-
state32.stepBudgetBlocks = 0;
|
|
12944
|
-
state32.noDiffWarnings = 0;
|
|
12945
|
-
state32.noDiffBlocks = 0;
|
|
12946
|
-
state32.repeatedErrorWarnings = 0;
|
|
12947
|
-
state32.repeatedErrorBlocks = 0;
|
|
12972
|
+
const final = aggregateRuns();
|
|
12973
|
+
state32.runs.clear();
|
|
12948
12974
|
api.log.info("loop-breaker: teardown complete", { final });
|
|
12949
12975
|
},
|
|
12950
12976
|
async health() {
|
|
12977
|
+
const totals = aggregateRuns();
|
|
12951
12978
|
return {
|
|
12952
12979
|
ok: true,
|
|
12953
|
-
message: `loop-breaker: ${
|
|
12980
|
+
message: `loop-breaker: ${totals.invocations} call(s) observed, ${totals.warnings + totals.noDiffWarnings + totals.repeatedErrorWarnings} warning(s), ${totals.blocks} block(s), ${totals.oscillationsDetected} oscillation(s)`,
|
|
12954
12981
|
counters: {
|
|
12955
|
-
invocations:
|
|
12956
|
-
postInvocations:
|
|
12957
|
-
warnings:
|
|
12958
|
-
blocks:
|
|
12959
|
-
oscillationsDetected:
|
|
12960
|
-
stepBudgetBlocks:
|
|
12961
|
-
noDiffWarnings:
|
|
12962
|
-
noDiffBlocks:
|
|
12963
|
-
repeatedErrorWarnings:
|
|
12964
|
-
repeatedErrorBlocks:
|
|
12982
|
+
invocations: totals.invocations,
|
|
12983
|
+
postInvocations: totals.postInvocations,
|
|
12984
|
+
warnings: totals.warnings,
|
|
12985
|
+
blocks: totals.blocks,
|
|
12986
|
+
oscillationsDetected: totals.oscillationsDetected,
|
|
12987
|
+
stepBudgetBlocks: totals.stepBudgetBlocks,
|
|
12988
|
+
noDiffWarnings: totals.noDiffWarnings,
|
|
12989
|
+
noDiffBlocks: totals.noDiffBlocks,
|
|
12990
|
+
repeatedErrorWarnings: totals.repeatedErrorWarnings,
|
|
12991
|
+
repeatedErrorBlocks: totals.repeatedErrorBlocks
|
|
12965
12992
|
}
|
|
12966
12993
|
};
|
|
12967
12994
|
}
|
|
@@ -13879,7 +13906,8 @@ var state35 = {
|
|
|
13879
13906
|
lastDelivery: null,
|
|
13880
13907
|
stopHookUnregister: null,
|
|
13881
13908
|
eventUnsubscribers: [],
|
|
13882
|
-
circuitWarned: false
|
|
13909
|
+
circuitWarned: false,
|
|
13910
|
+
disabledReason: null
|
|
13883
13911
|
};
|
|
13884
13912
|
var KNOWN_EVENTS = ["session.stop", "tool.error", "budget.threshold"];
|
|
13885
13913
|
var DEFAULTS33 = {
|
|
@@ -14062,13 +14090,21 @@ var plugin38 = {
|
|
|
14062
14090
|
}
|
|
14063
14091
|
}
|
|
14064
14092
|
state35.eventUnsubscribers = [];
|
|
14065
|
-
|
|
14093
|
+
state35.disabledReason = null;
|
|
14094
|
+
const rawCfg = api.config.extensions?.["notify-hub"];
|
|
14095
|
+
const cfg = readConfig33(rawCfg);
|
|
14066
14096
|
let active = cfg.enabled && cfg.webhookUrl.length > 0;
|
|
14097
|
+
const rawUrl = rawCfg?.["webhookUrl"] ?? rawCfg?.["webhook_url"] ?? rawCfg?.["url"];
|
|
14098
|
+
if (cfg.enabled && typeof rawUrl === "string" && rawUrl.trim() && !cfg.webhookUrl) {
|
|
14099
|
+
state35.disabledReason = "webhookUrl was refused: it must be an http(s) URL without credentials that does not target localhost or a private address";
|
|
14100
|
+
api.log.warn(`notify-hub: ${state35.disabledReason}`);
|
|
14101
|
+
}
|
|
14067
14102
|
if (active) {
|
|
14068
14103
|
try {
|
|
14069
14104
|
const url = new URL(cfg.webhookUrl);
|
|
14070
14105
|
const hostnameBlocked = await hasPrivateResolvedIP(url.hostname);
|
|
14071
14106
|
if (hostnameBlocked) {
|
|
14107
|
+
state35.disabledReason = `webhookUrl was refused: ${url.hostname} resolves to a private/local IP`;
|
|
14072
14108
|
api.log.warn(
|
|
14073
14109
|
`notify-hub: webhook URL resolves to a private/local IP (${url.hostname}) \u2014 disabling plugin`
|
|
14074
14110
|
);
|
|
@@ -14141,7 +14177,7 @@ var plugin38 = {
|
|
|
14141
14177
|
const ch = state35.channel;
|
|
14142
14178
|
if (!ch) {
|
|
14143
14179
|
throw new Error(
|
|
14144
|
-
'no webhookUrl configured \u2014 set config.extensions["notify-hub"].webhookUrl to enable deliveries'
|
|
14180
|
+
state35.disabledReason ?? 'no webhookUrl configured \u2014 set config.extensions["notify-hub"].webhookUrl to enable deliveries'
|
|
14145
14181
|
);
|
|
14146
14182
|
}
|
|
14147
14183
|
const inp = input ?? {};
|
|
@@ -17401,7 +17437,7 @@ var state41 = {
|
|
|
17401
17437
|
lastHookWarning: new runtime_exports.BoundedMap({ max: 512, ttlMs: HOOK_WARNING_COOLDOWN_MS2 })
|
|
17402
17438
|
};
|
|
17403
17439
|
var DEFAULTS38 = {
|
|
17404
|
-
enabled:
|
|
17440
|
+
enabled: true,
|
|
17405
17441
|
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
17406
17442
|
maxSuggestions: 5,
|
|
17407
17443
|
rules: { longFunctionLines: 50, maxParams: 5, maxNesting: 3 }
|
|
@@ -17592,7 +17628,7 @@ var plugin44 = {
|
|
|
17592
17628
|
configSchema: {
|
|
17593
17629
|
type: "object",
|
|
17594
17630
|
properties: {
|
|
17595
|
-
enabled: { type: "boolean", default:
|
|
17631
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
17596
17632
|
extensions: {
|
|
17597
17633
|
type: "array",
|
|
17598
17634
|
items: { type: "string" },
|
|
@@ -19007,7 +19043,7 @@ var state44 = {
|
|
|
19007
19043
|
hookUnregister: null
|
|
19008
19044
|
};
|
|
19009
19045
|
var DEFAULTS42 = {
|
|
19010
|
-
enabled:
|
|
19046
|
+
enabled: true,
|
|
19011
19047
|
severity: "warn",
|
|
19012
19048
|
maxFindings: 10,
|
|
19013
19049
|
scanOnChange: [
|
|
@@ -19198,7 +19234,7 @@ var plugin48 = {
|
|
|
19198
19234
|
configSchema: {
|
|
19199
19235
|
type: "object",
|
|
19200
19236
|
properties: {
|
|
19201
|
-
enabled: { type: "boolean", default:
|
|
19237
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
19202
19238
|
severity: {
|
|
19203
19239
|
type: "string",
|
|
19204
19240
|
enum: ["warn", "block"],
|