@wrongstack/plugins 1.0.13 → 1.0.14

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 CHANGED
@@ -699,7 +699,7 @@ once you're confident in the rewrite.
699
699
  ### 22 — `loop-breaker`
700
700
 
701
701
  **Tools**: `loop_breaker_status`
702
- **Hooks**: `PreToolUse`
702
+ **Hooks**: `PreToolUse`, `PostToolUse`, `UserPromptSubmit`
703
703
 
704
704
  Detects repeated tool calls before they execute. It tracks exact-repeat
705
705
  streaks and A-B-A-B oscillations, then either blocks the call or injects a
@@ -711,6 +711,7 @@ warning depending on `mode`.
711
711
  "loop-breaker": {
712
712
  "enabled": true,
713
713
  "mode": "block", // "block" | "warn"
714
+ "maxSteps": 0, // unlimited; set a positive value to opt into a cap
714
715
  "repeatThreshold": 3,
715
716
  "oscillationThreshold": 2
716
717
  }
package/dist/index.js CHANGED
@@ -12465,29 +12465,70 @@ var llm_cache_default = plugin34;
12465
12465
  // src/loop-breaker/index.ts
12466
12466
  import { execFile as execFile8 } from "node:child_process";
12467
12467
  import { isAbsolute as isAbsolute15, relative as relative15 } from "node:path";
12468
- var state32 = {
12469
- lastFingerprint: null,
12470
- streak: 0,
12471
- recent: [],
12472
- pendingBlockReason: null,
12473
- lastDiffFingerprint: null,
12474
- noDiffStreak: 0,
12475
- lastErrorFingerprint: null,
12476
- repeatedErrorStreak: 0,
12477
- invocations: 0,
12478
- postInvocations: 0,
12479
- warnings: 0,
12480
- blocks: 0,
12481
- oscillationsDetected: 0,
12482
- stepBudgetBlocks: 0,
12483
- noDiffWarnings: 0,
12484
- noDiffBlocks: 0,
12485
- repeatedErrorWarnings: 0,
12486
- repeatedErrorBlocks: 0,
12487
- hookUnregister: null
12488
- };
12468
+ var FALLBACK_SESSION = "__default__";
12469
+ var MAX_TRACKED_RUNS = 32;
12470
+ function createRunState() {
12471
+ return {
12472
+ lastFingerprint: null,
12473
+ streak: 0,
12474
+ recent: [],
12475
+ pendingBlockReason: null,
12476
+ lastDiffFingerprint: null,
12477
+ noDiffStreak: 0,
12478
+ lastErrorFingerprint: null,
12479
+ repeatedErrorStreak: 0,
12480
+ invocations: 0,
12481
+ postInvocations: 0,
12482
+ warnings: 0,
12483
+ blocks: 0,
12484
+ oscillationsDetected: 0,
12485
+ stepBudgetBlocks: 0,
12486
+ noDiffWarnings: 0,
12487
+ noDiffBlocks: 0,
12488
+ repeatedErrorWarnings: 0,
12489
+ repeatedErrorBlocks: 0
12490
+ };
12491
+ }
12492
+ var state32 = { runs: /* @__PURE__ */ new Map(), hookUnregister: null };
12493
+ function sessionKey(sessionId) {
12494
+ return sessionId || FALLBACK_SESSION;
12495
+ }
12496
+ function storeRun(key, current) {
12497
+ state32.runs.set(key, current);
12498
+ if (state32.runs.size <= MAX_TRACKED_RUNS) return;
12499
+ const oldest = state32.runs.keys().next().value;
12500
+ if (oldest !== void 0 && oldest !== key) state32.runs.delete(oldest);
12501
+ }
12502
+ function runState(sessionId) {
12503
+ const key = sessionKey(sessionId);
12504
+ let current = state32.runs.get(key);
12505
+ if (!current) {
12506
+ current = createRunState();
12507
+ storeRun(key, current);
12508
+ }
12509
+ return current;
12510
+ }
12511
+ function resetRun(sessionId) {
12512
+ storeRun(sessionKey(sessionId), createRunState());
12513
+ }
12514
+ function aggregateRuns() {
12515
+ const total = createRunState();
12516
+ for (const current of state32.runs.values()) {
12517
+ total.invocations += current.invocations;
12518
+ total.postInvocations += current.postInvocations;
12519
+ total.warnings += current.warnings;
12520
+ total.blocks += current.blocks;
12521
+ total.oscillationsDetected += current.oscillationsDetected;
12522
+ total.stepBudgetBlocks += current.stepBudgetBlocks;
12523
+ total.noDiffWarnings += current.noDiffWarnings;
12524
+ total.noDiffBlocks += current.noDiffBlocks;
12525
+ total.repeatedErrorWarnings += current.repeatedErrorWarnings;
12526
+ total.repeatedErrorBlocks += current.repeatedErrorBlocks;
12527
+ }
12528
+ return total;
12529
+ }
12489
12530
  var DEFAULTS30 = {
12490
- enabled: true,
12531
+ enabled: false,
12491
12532
  // Documented contract (feature matrix, plugin description): warn, then
12492
12533
  // block. A warn-only default meant an agent stuck re-issuing the same call
12493
12534
  // was never actually stopped unless the user happened to set any option.
@@ -12495,7 +12536,7 @@ var DEFAULTS30 = {
12495
12536
  warnAfter: 3,
12496
12537
  blockAfter: 5,
12497
12538
  oscillationWindow: 8,
12498
- maxSteps: 200,
12539
+ maxSteps: 0,
12499
12540
  noDiffWarnAfter: 6,
12500
12541
  noDiffBlockAfter: 10,
12501
12542
  repeatedErrorWarnAfter: 2,
@@ -12521,7 +12562,7 @@ function readConfig30(raw) {
12521
12562
  const rawOsc = r["oscillationWindow"] ?? r["oscillation_window"] ?? r["window"];
12522
12563
  const rawMaxSteps = r["maxSteps"] ?? r["max_steps"] ?? r["stepLimit"] ?? r["step_limit"];
12523
12564
  return {
12524
- enabled: r["enabled"] !== false,
12565
+ enabled: r["enabled"] === true,
12525
12566
  mode,
12526
12567
  warnAfter,
12527
12568
  blockAfter,
@@ -12634,7 +12675,11 @@ var plugin35 = {
12634
12675
  configSchema: {
12635
12676
  type: "object",
12636
12677
  properties: {
12637
- enabled: { type: "boolean", default: true, description: "Master switch." },
12678
+ enabled: {
12679
+ type: "boolean",
12680
+ default: false,
12681
+ description: "Opt-in master switch; disabled unless explicitly enabled."
12682
+ },
12638
12683
  mode: {
12639
12684
  type: "string",
12640
12685
  enum: ["warn", "block"],
@@ -12662,8 +12707,8 @@ var plugin35 = {
12662
12707
  maxSteps: {
12663
12708
  type: "number",
12664
12709
  minimum: 0,
12665
- default: 200,
12666
- description: "Maximum tool steps before blocking; 0 disables the step budget."
12710
+ default: 0,
12711
+ description: "Optional maximum tool steps before blocking; 0 keeps runs unlimited."
12667
12712
  },
12668
12713
  noDiffWarnAfter: {
12669
12714
  type: "number",
@@ -12698,24 +12743,7 @@ var plugin35 = {
12698
12743
  }
12699
12744
  },
12700
12745
  setup(api) {
12701
- state32.lastFingerprint = null;
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;
12746
+ state32.runs.clear();
12719
12747
  if (state32.hookUnregister) {
12720
12748
  try {
12721
12749
  state32.hookUnregister();
@@ -12728,56 +12756,57 @@ var plugin35 = {
12728
12756
  if (!cfg.enabled) return;
12729
12757
  const toolName = input.toolName ?? "unknown";
12730
12758
  if (ignoreToolsSet.has(toolName)) return;
12731
- state32.invocations += 1;
12732
- if (state32.pendingBlockReason && cfg.mode === "block") {
12733
- const reason = state32.pendingBlockReason;
12734
- state32.pendingBlockReason = null;
12735
- state32.blocks += 1;
12759
+ const current = runState(input.sessionId);
12760
+ if (current.pendingBlockReason && cfg.mode === "block") {
12761
+ const reason = current.pendingBlockReason;
12762
+ current.pendingBlockReason = null;
12763
+ current.blocks += 1;
12736
12764
  api.metrics.counter("blocks");
12737
12765
  return { decision: "block", reason };
12738
12766
  }
12739
- if (cfg.maxSteps > 0 && state32.invocations > cfg.maxSteps && cfg.mode === "block") {
12740
- state32.blocks += 1;
12741
- state32.stepBudgetBlocks += 1;
12767
+ if (cfg.maxSteps > 0 && current.invocations >= cfg.maxSteps && cfg.mode === "block") {
12768
+ current.blocks += 1;
12769
+ current.stepBudgetBlocks += 1;
12742
12770
  api.metrics.counter("blocks");
12743
12771
  api.metrics.counter("step_budget_blocks");
12744
12772
  return {
12745
12773
  decision: "block",
12746
- reason: `loop-breaker: step budget exceeded (${state32.invocations}/${cfg.maxSteps} tool calls). Stop and report what has been tried instead of continuing an unbounded loop.`
12774
+ 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
12775
  };
12748
12776
  }
12777
+ current.invocations += 1;
12749
12778
  const fp = fingerprint(toolName, input.toolInput);
12750
- if (fp === state32.lastFingerprint) {
12751
- state32.streak += 1;
12779
+ if (fp === current.lastFingerprint) {
12780
+ current.streak += 1;
12752
12781
  } else {
12753
- state32.lastFingerprint = fp;
12754
- state32.streak = 1;
12782
+ current.lastFingerprint = fp;
12783
+ current.streak = 1;
12755
12784
  }
12756
- state32.recent.push(fp);
12757
- if (state32.recent.length > Math.max(cfg.oscillationWindow, 16)) {
12758
- state32.recent.splice(0, state32.recent.length - Math.max(cfg.oscillationWindow, 16));
12785
+ current.recent.push(fp);
12786
+ if (current.recent.length > Math.max(cfg.oscillationWindow, 16)) {
12787
+ current.recent.splice(0, current.recent.length - Math.max(cfg.oscillationWindow, 16));
12759
12788
  }
12760
- if (state32.streak >= cfg.blockAfter && cfg.mode === "block") {
12761
- state32.blocks += 1;
12789
+ if (current.streak >= cfg.blockAfter && cfg.mode === "block") {
12790
+ current.blocks += 1;
12762
12791
  api.metrics.counter("blocks");
12763
12792
  return {
12764
12793
  decision: "block",
12765
- reason: `loop-breaker: "${toolName}" has been called ${state32.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.)`
12794
+ 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
12795
  };
12767
12796
  }
12768
- if (state32.streak >= cfg.warnAfter) {
12769
- state32.warnings += 1;
12797
+ if (current.streak >= cfg.warnAfter) {
12798
+ current.warnings += 1;
12770
12799
  api.metrics.counter("warnings");
12771
- const remaining = cfg.mode === "block" ? cfg.blockAfter - state32.streak : null;
12800
+ const remaining = cfg.mode === "block" ? cfg.blockAfter - current.streak : null;
12772
12801
  return {
12773
12802
  decision: "allow",
12774
- additionalContext: `loop-breaker: "${toolName}" repeated ${state32.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."
12803
+ 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
12804
  };
12776
12805
  }
12777
- if (isOscillating(state32.recent, cfg.oscillationWindow)) {
12778
- state32.oscillationsDetected += 1;
12806
+ if (isOscillating(current.recent, cfg.oscillationWindow)) {
12807
+ current.oscillationsDetected += 1;
12779
12808
  api.metrics.counter("oscillations");
12780
- state32.recent = [];
12809
+ current.recent = [];
12781
12810
  return {
12782
12811
  decision: "allow",
12783
12812
  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 +12818,33 @@ var plugin35 = {
12789
12818
  if (!cfg.enabled) return;
12790
12819
  const toolName = input.toolName ?? "unknown";
12791
12820
  if (ignoreToolsSet.has(toolName)) return;
12792
- state32.postInvocations += 1;
12821
+ const current = runState(input.sessionId);
12822
+ current.postInvocations += 1;
12793
12823
  if (input.toolResult?.isError) {
12794
12824
  const errorFingerprint = normalizeError(input.toolResult.content);
12795
- if (errorFingerprint && errorFingerprint === state32.lastErrorFingerprint) {
12796
- state32.repeatedErrorStreak += 1;
12825
+ if (errorFingerprint && errorFingerprint === current.lastErrorFingerprint) {
12826
+ current.repeatedErrorStreak += 1;
12797
12827
  } else {
12798
- state32.lastErrorFingerprint = errorFingerprint;
12799
- state32.repeatedErrorStreak = errorFingerprint ? 1 : 0;
12828
+ current.lastErrorFingerprint = errorFingerprint;
12829
+ current.repeatedErrorStreak = errorFingerprint ? 1 : 0;
12800
12830
  }
12801
- state32.noDiffStreak = 0;
12802
- if (cfg.repeatedErrorBlockAfter > 0 && state32.repeatedErrorStreak >= cfg.repeatedErrorBlockAfter && cfg.mode === "block") {
12803
- state32.repeatedErrorBlocks += 1;
12804
- state32.pendingBlockReason = `loop-breaker: the same tool error repeated ${state32.repeatedErrorStreak} times. The next tool call is blocked so you can stop, summarize the repeated failure, and change approach.`;
12831
+ current.noDiffStreak = 0;
12832
+ if (cfg.repeatedErrorBlockAfter > 0 && current.repeatedErrorStreak >= cfg.repeatedErrorBlockAfter && cfg.mode === "block") {
12833
+ current.repeatedErrorBlocks += 1;
12834
+ 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
12835
  }
12806
- if (state32.repeatedErrorStreak >= cfg.repeatedErrorWarnAfter) {
12807
- state32.repeatedErrorWarnings += 1;
12836
+ if (current.repeatedErrorStreak >= cfg.repeatedErrorWarnAfter) {
12837
+ current.repeatedErrorWarnings += 1;
12808
12838
  api.metrics.counter("repeated_error_warnings");
12809
12839
  return {
12810
12840
  decision: "allow",
12811
- additionalContext: `loop-breaker: same error repeated ${state32.repeatedErrorStreak}x. Do not retry the same command/tool unchanged; inspect the root cause or ask for help.`
12841
+ 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
12842
  };
12813
12843
  }
12814
12844
  return;
12815
12845
  }
12816
- state32.lastErrorFingerprint = null;
12817
- state32.repeatedErrorStreak = 0;
12846
+ current.lastErrorFingerprint = null;
12847
+ current.repeatedErrorStreak = 0;
12818
12848
  if (!MUTATING_TOOLS.has(toolName)) return;
12819
12849
  const toolInput = input.toolInput ?? {};
12820
12850
  const rawTarget = toolInput["path"] ?? toolInput["TargetFile"] ?? toolInput["filePath"] ?? toolInput["targetFile"] ?? toolInput["file_path"] ?? toolInput["destination"] ?? toolInput["file"];
@@ -12826,22 +12856,22 @@ var plugin35 = {
12826
12856
  runtime.signal
12827
12857
  );
12828
12858
  if (diffFingerprint === null) return;
12829
- if (diffFingerprint === state32.lastDiffFingerprint) {
12830
- state32.noDiffStreak += 1;
12859
+ if (diffFingerprint === current.lastDiffFingerprint) {
12860
+ current.noDiffStreak += 1;
12831
12861
  } else {
12832
- state32.lastDiffFingerprint = diffFingerprint;
12833
- state32.noDiffStreak = 0;
12862
+ current.lastDiffFingerprint = diffFingerprint;
12863
+ current.noDiffStreak = 0;
12834
12864
  }
12835
- if (cfg.noDiffBlockAfter > 0 && state32.noDiffStreak >= cfg.noDiffBlockAfter && cfg.mode === "block") {
12836
- state32.noDiffBlocks += 1;
12837
- state32.pendingBlockReason = `loop-breaker: no diff was produced in the last ${state32.noDiffStreak} mutating step(s). The next tool call is blocked because continued edits are not changing the working tree.`;
12865
+ if (cfg.noDiffBlockAfter > 0 && current.noDiffStreak >= cfg.noDiffBlockAfter && cfg.mode === "block") {
12866
+ current.noDiffBlocks += 1;
12867
+ 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
12868
  }
12839
- if (state32.noDiffStreak >= cfg.noDiffWarnAfter) {
12840
- state32.noDiffWarnings += 1;
12869
+ if (current.noDiffStreak >= cfg.noDiffWarnAfter) {
12870
+ current.noDiffWarnings += 1;
12841
12871
  api.metrics.counter("no_diff_warnings");
12842
12872
  return {
12843
12873
  decision: "allow",
12844
- additionalContext: `loop-breaker: no diff has changed for ${state32.noDiffStreak} mutating step(s). Stop repeating edits; read the file/status, explain why no change is happening, or choose a different approach.`
12874
+ 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
12875
  };
12846
12876
  }
12847
12877
  return;
@@ -12856,9 +12886,18 @@ var plugin35 = {
12856
12886
  timeoutMs: 2e3,
12857
12887
  failurePolicy: "open"
12858
12888
  });
12889
+ const unregisterPrompt = api.registerHook(
12890
+ "UserPromptSubmit",
12891
+ void 0,
12892
+ ((input) => {
12893
+ resetRun(input.sessionId);
12894
+ }),
12895
+ { name: "loop-breaker-turn-reset", failurePolicy: "open" }
12896
+ );
12859
12897
  state32.hookUnregister = () => {
12860
12898
  unregisterPre();
12861
12899
  unregisterPost();
12900
+ unregisterPrompt();
12862
12901
  };
12863
12902
  api.tools.register({
12864
12903
  name: "loop_breaker_status",
@@ -12867,7 +12906,8 @@ var plugin35 = {
12867
12906
  permission: "auto",
12868
12907
  category: "Diagnostics",
12869
12908
  mutating: false,
12870
- async execute() {
12909
+ async execute(_input, ctx) {
12910
+ const current = runState(ctx?.session?.id);
12871
12911
  return {
12872
12912
  ok: true,
12873
12913
  enabled: cfg.enabled,
@@ -12881,20 +12921,20 @@ var plugin35 = {
12881
12921
  repeatedErrorWarnAfter: cfg.repeatedErrorWarnAfter,
12882
12922
  repeatedErrorBlockAfter: cfg.repeatedErrorBlockAfter,
12883
12923
  ignoreTools: cfg.ignoreTools,
12884
- currentStreak: state32.streak,
12885
- noDiffStreak: state32.noDiffStreak,
12886
- repeatedErrorStreak: state32.repeatedErrorStreak,
12924
+ currentStreak: current.streak,
12925
+ noDiffStreak: current.noDiffStreak,
12926
+ repeatedErrorStreak: current.repeatedErrorStreak,
12887
12927
  counters: {
12888
- invocations: state32.invocations,
12889
- postInvocations: state32.postInvocations,
12890
- warnings: state32.warnings,
12891
- blocks: state32.blocks,
12892
- oscillationsDetected: state32.oscillationsDetected,
12893
- stepBudgetBlocks: state32.stepBudgetBlocks,
12894
- noDiffWarnings: state32.noDiffWarnings,
12895
- noDiffBlocks: state32.noDiffBlocks,
12896
- repeatedErrorWarnings: state32.repeatedErrorWarnings,
12897
- repeatedErrorBlocks: state32.repeatedErrorBlocks
12928
+ invocations: current.invocations,
12929
+ postInvocations: current.postInvocations,
12930
+ warnings: current.warnings,
12931
+ blocks: current.blocks,
12932
+ oscillationsDetected: current.oscillationsDetected,
12933
+ stepBudgetBlocks: current.stepBudgetBlocks,
12934
+ noDiffWarnings: current.noDiffWarnings,
12935
+ noDiffBlocks: current.noDiffBlocks,
12936
+ repeatedErrorWarnings: current.repeatedErrorWarnings,
12937
+ repeatedErrorBlocks: current.repeatedErrorBlocks
12898
12938
  }
12899
12939
  };
12900
12940
  }
@@ -12915,53 +12955,26 @@ var plugin35 = {
12915
12955
  }
12916
12956
  state32.hookUnregister = null;
12917
12957
  }
12918
- const final = {
12919
- invocations: state32.invocations,
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;
12958
+ const final = aggregateRuns();
12959
+ state32.runs.clear();
12948
12960
  api.log.info("loop-breaker: teardown complete", { final });
12949
12961
  },
12950
12962
  async health() {
12963
+ const totals = aggregateRuns();
12951
12964
  return {
12952
12965
  ok: true,
12953
- message: `loop-breaker: ${state32.invocations} call(s) observed, ${state32.warnings + state32.noDiffWarnings + state32.repeatedErrorWarnings} warning(s), ${state32.blocks} block(s), ${state32.oscillationsDetected} oscillation(s)`,
12966
+ 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
12967
  counters: {
12955
- invocations: state32.invocations,
12956
- postInvocations: state32.postInvocations,
12957
- warnings: state32.warnings,
12958
- blocks: state32.blocks,
12959
- oscillationsDetected: state32.oscillationsDetected,
12960
- stepBudgetBlocks: state32.stepBudgetBlocks,
12961
- noDiffWarnings: state32.noDiffWarnings,
12962
- noDiffBlocks: state32.noDiffBlocks,
12963
- repeatedErrorWarnings: state32.repeatedErrorWarnings,
12964
- repeatedErrorBlocks: state32.repeatedErrorBlocks
12968
+ invocations: totals.invocations,
12969
+ postInvocations: totals.postInvocations,
12970
+ warnings: totals.warnings,
12971
+ blocks: totals.blocks,
12972
+ oscillationsDetected: totals.oscillationsDetected,
12973
+ stepBudgetBlocks: totals.stepBudgetBlocks,
12974
+ noDiffWarnings: totals.noDiffWarnings,
12975
+ noDiffBlocks: totals.noDiffBlocks,
12976
+ repeatedErrorWarnings: totals.repeatedErrorWarnings,
12977
+ repeatedErrorBlocks: totals.repeatedErrorBlocks
12965
12978
  }
12966
12979
  };
12967
12980
  }
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/plugins",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
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.14",
307
+ "@wrongstack/plugin-sdk": "1.0.14",
308
+ "@wrongstack/primitives": "1.0.14",
309
+ "@wrongstack/tools": "1.0.14"
310
310
  },
311
311
  "scripts": {
312
312
  "build": "node ../../scripts/build-package.mjs",