@wrongstack/plugins 0.281.1 → 0.282.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -3
- package/dist/accessibility-auditor.d.ts +40 -0
- package/dist/accessibility-auditor.js +411 -0
- package/dist/agent-handoff.d.ts +37 -0
- package/dist/agent-handoff.js +298 -0
- package/dist/api-compatibility-gate.d.ts +38 -0
- package/dist/api-compatibility-gate.js +357 -0
- package/dist/auto-i18n-extractor.d.ts +36 -0
- package/dist/auto-i18n-extractor.js +335 -0
- package/dist/checkpoint.js +18 -0
- package/dist/code-metrics.d.ts +31 -0
- package/dist/code-metrics.js +338 -0
- package/dist/commit-validator.js +67 -12
- package/dist/cost-tracker.js +58 -16
- package/dist/dead-code-detector.d.ts +34 -0
- package/dist/dead-code-detector.js +354 -0
- package/dist/dep-guard.js +47 -6
- package/dist/dependency-vulnerability-gate.d.ts +35 -0
- package/dist/dependency-vulnerability-gate.js +308 -0
- package/dist/diff-summary.js +97 -10
- package/dist/doc-sync-guard.d.ts +33 -0
- package/dist/doc-sync-guard.js +223 -0
- package/dist/duplicate-code-detector.d.ts +33 -0
- package/dist/duplicate-code-detector.js +384 -0
- package/dist/feature-flag-tracker.d.ts +38 -0
- package/dist/feature-flag-tracker.js +316 -0
- package/dist/file-watcher.js +85 -36
- package/dist/format-on-save.js +76 -10
- package/dist/import-organizer.js +73 -14
- package/dist/index.d.ts +27 -0
- package/dist/index.js +14067 -5054
- package/dist/interface-contract-guard.d.ts +37 -0
- package/dist/interface-contract-guard.js +302 -0
- package/dist/knowledge-graph.d.ts +45 -0
- package/dist/knowledge-graph.js +325 -0
- package/dist/license-audit-gate.d.ts +34 -0
- package/dist/license-audit-gate.js +260 -0
- package/dist/llm-cache.js +5 -0
- package/dist/loop-breaker.d.ts +0 -38
- package/dist/loop-breaker.js +209 -8
- package/dist/migration-planner.d.ts +30 -0
- package/dist/migration-planner.js +349 -0
- package/dist/model-router.js +5 -0
- package/dist/performance-regression-gate.d.ts +33 -0
- package/dist/performance-regression-gate.js +315 -0
- package/dist/plugin-stack-observer.d.ts +35 -0
- package/dist/plugin-stack-observer.js +125 -0
- package/dist/pr-drafter.d.ts +35 -0
- package/dist/pr-drafter.js +334 -0
- package/dist/prompt-firewall.js +5 -0
- package/dist/refactor-suggester.d.ts +38 -0
- package/dist/refactor-suggester.js +382 -0
- package/dist/release-notes-generator.d.ts +27 -0
- package/dist/release-notes-generator.js +209 -0
- package/dist/schema-evolution-guard.d.ts +42 -0
- package/dist/schema-evolution-guard.js +319 -0
- package/dist/security-hotspot-scanner.d.ts +30 -0
- package/dist/security-hotspot-scanner.js +402 -0
- package/dist/semantic-search-indexer.d.ts +38 -0
- package/dist/semantic-search-indexer.js +436 -0
- package/dist/shell-check.js +38 -3
- package/dist/smart-rename.d.ts +26 -0
- package/dist/smart-rename.js +170 -0
- package/dist/spec-linker.js +273 -133
- package/dist/test-coverage-gate.d.ts +37 -0
- package/dist/test-coverage-gate.js +263 -0
- package/dist/test-flake-detector.d.ts +27 -0
- package/dist/test-flake-detector.js +274 -0
- package/dist/test-generator.d.ts +32 -0
- package/dist/test-generator.js +243 -0
- package/dist/test-runner-gate.js +154 -22
- package/dist/todo-listener.d.ts +2 -2
- package/dist/todo-listener.js +5 -5
- package/dist/token-throttle.js +5 -0
- package/dist/type-gate.d.ts +37 -0
- package/dist/type-gate.js +311 -0
- package/package.json +112 -4
- package/LICENSE +0 -21
package/dist/loop-breaker.js
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
|
|
1
3
|
// src/loop-breaker/index.ts
|
|
2
4
|
var state = {
|
|
3
5
|
lastFingerprint: null,
|
|
4
6
|
streak: 0,
|
|
5
7
|
recent: [],
|
|
8
|
+
pendingBlockReason: null,
|
|
9
|
+
lastDiffFingerprint: null,
|
|
10
|
+
noDiffStreak: 0,
|
|
11
|
+
lastErrorFingerprint: null,
|
|
12
|
+
repeatedErrorStreak: 0,
|
|
6
13
|
invocations: 0,
|
|
14
|
+
postInvocations: 0,
|
|
7
15
|
warnings: 0,
|
|
8
16
|
blocks: 0,
|
|
9
17
|
oscillationsDetected: 0,
|
|
18
|
+
stepBudgetBlocks: 0,
|
|
19
|
+
noDiffWarnings: 0,
|
|
20
|
+
noDiffBlocks: 0,
|
|
21
|
+
repeatedErrorWarnings: 0,
|
|
22
|
+
repeatedErrorBlocks: 0,
|
|
10
23
|
hookUnregister: null
|
|
11
24
|
};
|
|
12
25
|
var DEFAULTS = {
|
|
13
26
|
enabled: true,
|
|
14
|
-
mode: "
|
|
27
|
+
mode: "warn",
|
|
15
28
|
warnAfter: 3,
|
|
16
29
|
blockAfter: 5,
|
|
17
30
|
oscillationWindow: 8,
|
|
31
|
+
maxSteps: 200,
|
|
32
|
+
noDiffWarnAfter: 6,
|
|
33
|
+
noDiffBlockAfter: 10,
|
|
34
|
+
repeatedErrorWarnAfter: 2,
|
|
35
|
+
repeatedErrorBlockAfter: 3,
|
|
18
36
|
ignoreTools: []
|
|
19
37
|
};
|
|
20
38
|
function readConfig(raw) {
|
|
@@ -28,6 +46,11 @@ function readConfig(raw) {
|
|
|
28
46
|
warnAfter,
|
|
29
47
|
blockAfter,
|
|
30
48
|
oscillationWindow: typeof r["oscillationWindow"] === "number" && r["oscillationWindow"] >= 4 ? r["oscillationWindow"] : DEFAULTS.oscillationWindow,
|
|
49
|
+
maxSteps: typeof r["maxSteps"] === "number" && r["maxSteps"] >= 0 ? Math.floor(r["maxSteps"]) : DEFAULTS.maxSteps,
|
|
50
|
+
noDiffWarnAfter: typeof r["noDiffWarnAfter"] === "number" && r["noDiffWarnAfter"] >= 1 ? Math.floor(r["noDiffWarnAfter"]) : DEFAULTS.noDiffWarnAfter,
|
|
51
|
+
noDiffBlockAfter: typeof r["noDiffBlockAfter"] === "number" && r["noDiffBlockAfter"] >= 0 ? Math.floor(r["noDiffBlockAfter"]) : DEFAULTS.noDiffBlockAfter,
|
|
52
|
+
repeatedErrorWarnAfter: typeof r["repeatedErrorWarnAfter"] === "number" && r["repeatedErrorWarnAfter"] >= 1 ? Math.floor(r["repeatedErrorWarnAfter"]) : DEFAULTS.repeatedErrorWarnAfter,
|
|
53
|
+
repeatedErrorBlockAfter: typeof r["repeatedErrorBlockAfter"] === "number" && r["repeatedErrorBlockAfter"] >= 0 ? Math.floor(r["repeatedErrorBlockAfter"]) : DEFAULTS.repeatedErrorBlockAfter,
|
|
31
54
|
ignoreTools: Array.isArray(r["ignoreTools"]) ? r["ignoreTools"].filter((t) => typeof t === "string") : []
|
|
32
55
|
};
|
|
33
56
|
}
|
|
@@ -62,6 +85,32 @@ function isOscillating(recent, windowSize) {
|
|
|
62
85
|
}
|
|
63
86
|
return window[0] !== window[1];
|
|
64
87
|
}
|
|
88
|
+
var MUTATING_TOOLS = /* @__PURE__ */ new Set(["edit", "write"]);
|
|
89
|
+
function hashString(value) {
|
|
90
|
+
let h = 5381;
|
|
91
|
+
const cap = Math.min(value.length, 1e6);
|
|
92
|
+
for (let i = 0; i < cap; i++) {
|
|
93
|
+
h = (h << 5) + h + value.charCodeAt(i) | 0;
|
|
94
|
+
}
|
|
95
|
+
return String(h >>> 0);
|
|
96
|
+
}
|
|
97
|
+
function gitDiffFingerprint(cwd) {
|
|
98
|
+
try {
|
|
99
|
+
const diff = execFileSync("git", ["diff", "--no-ext-diff", "--"], {
|
|
100
|
+
cwd,
|
|
101
|
+
encoding: "utf8",
|
|
102
|
+
timeout: 1e3,
|
|
103
|
+
maxBuffer: 1e6,
|
|
104
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
105
|
+
});
|
|
106
|
+
return diff.length === 0 ? "" : hashString(diff);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function normalizeError(content) {
|
|
112
|
+
return content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, 12).join("\n").replace(/\b\d{2,}\b/g, "<n>").slice(0, 1e3);
|
|
113
|
+
}
|
|
65
114
|
var plugin = {
|
|
66
115
|
name: "loop-breaker",
|
|
67
116
|
version: "0.1.0",
|
|
@@ -75,8 +124,8 @@ var plugin = {
|
|
|
75
124
|
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
76
125
|
mode: {
|
|
77
126
|
type: "string",
|
|
78
|
-
enum: ["
|
|
79
|
-
default: "
|
|
127
|
+
enum: ["warn", "block"],
|
|
128
|
+
default: "warn",
|
|
80
129
|
description: "block = refuse the repeated call; warn = only inject context."
|
|
81
130
|
},
|
|
82
131
|
warnAfter: {
|
|
@@ -97,6 +146,36 @@ var plugin = {
|
|
|
97
146
|
default: 8,
|
|
98
147
|
description: "Recent-call window length used for A-B-A-B oscillation detection."
|
|
99
148
|
},
|
|
149
|
+
maxSteps: {
|
|
150
|
+
type: "number",
|
|
151
|
+
minimum: 0,
|
|
152
|
+
default: 200,
|
|
153
|
+
description: "Maximum tool steps before blocking; 0 disables the step budget."
|
|
154
|
+
},
|
|
155
|
+
noDiffWarnAfter: {
|
|
156
|
+
type: "number",
|
|
157
|
+
minimum: 1,
|
|
158
|
+
default: 6,
|
|
159
|
+
description: "Successful edit/write steps without a changed git diff before warning."
|
|
160
|
+
},
|
|
161
|
+
noDiffBlockAfter: {
|
|
162
|
+
type: "number",
|
|
163
|
+
minimum: 0,
|
|
164
|
+
default: 10,
|
|
165
|
+
description: "Successful edit/write steps without a changed git diff before blocking next step; 0 disables."
|
|
166
|
+
},
|
|
167
|
+
repeatedErrorWarnAfter: {
|
|
168
|
+
type: "number",
|
|
169
|
+
minimum: 1,
|
|
170
|
+
default: 2,
|
|
171
|
+
description: "Consecutive identical tool errors before warning."
|
|
172
|
+
},
|
|
173
|
+
repeatedErrorBlockAfter: {
|
|
174
|
+
type: "number",
|
|
175
|
+
minimum: 0,
|
|
176
|
+
default: 3,
|
|
177
|
+
description: "Consecutive identical tool errors before blocking next step; 0 disables."
|
|
178
|
+
},
|
|
100
179
|
ignoreTools: {
|
|
101
180
|
type: "array",
|
|
102
181
|
items: { type: "string" },
|
|
@@ -109,10 +188,21 @@ var plugin = {
|
|
|
109
188
|
state.lastFingerprint = null;
|
|
110
189
|
state.streak = 0;
|
|
111
190
|
state.recent = [];
|
|
191
|
+
state.pendingBlockReason = null;
|
|
192
|
+
state.lastDiffFingerprint = null;
|
|
193
|
+
state.noDiffStreak = 0;
|
|
194
|
+
state.lastErrorFingerprint = null;
|
|
195
|
+
state.repeatedErrorStreak = 0;
|
|
112
196
|
state.invocations = 0;
|
|
197
|
+
state.postInvocations = 0;
|
|
113
198
|
state.warnings = 0;
|
|
114
199
|
state.blocks = 0;
|
|
115
200
|
state.oscillationsDetected = 0;
|
|
201
|
+
state.stepBudgetBlocks = 0;
|
|
202
|
+
state.noDiffWarnings = 0;
|
|
203
|
+
state.noDiffBlocks = 0;
|
|
204
|
+
state.repeatedErrorWarnings = 0;
|
|
205
|
+
state.repeatedErrorBlocks = 0;
|
|
116
206
|
if (state.hookUnregister) {
|
|
117
207
|
try {
|
|
118
208
|
state.hookUnregister();
|
|
@@ -126,6 +216,23 @@ var plugin = {
|
|
|
126
216
|
const toolName = input.toolName ?? "unknown";
|
|
127
217
|
if (cfg.ignoreTools.includes(toolName)) return;
|
|
128
218
|
state.invocations += 1;
|
|
219
|
+
if (state.pendingBlockReason && cfg.mode === "block") {
|
|
220
|
+
const reason = state.pendingBlockReason;
|
|
221
|
+
state.pendingBlockReason = null;
|
|
222
|
+
state.blocks += 1;
|
|
223
|
+
api.metrics.counter("blocks");
|
|
224
|
+
return { decision: "block", reason };
|
|
225
|
+
}
|
|
226
|
+
if (cfg.maxSteps > 0 && state.invocations > cfg.maxSteps && cfg.mode === "block") {
|
|
227
|
+
state.blocks += 1;
|
|
228
|
+
state.stepBudgetBlocks += 1;
|
|
229
|
+
api.metrics.counter("blocks");
|
|
230
|
+
api.metrics.counter("step_budget_blocks");
|
|
231
|
+
return {
|
|
232
|
+
decision: "block",
|
|
233
|
+
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.`
|
|
234
|
+
};
|
|
235
|
+
}
|
|
129
236
|
const fp = fingerprint(toolName, input.toolInput);
|
|
130
237
|
if (fp === state.lastFingerprint) {
|
|
131
238
|
state.streak += 1;
|
|
@@ -165,7 +272,65 @@ var plugin = {
|
|
|
165
272
|
}
|
|
166
273
|
return;
|
|
167
274
|
};
|
|
168
|
-
|
|
275
|
+
const postHook = (input) => {
|
|
276
|
+
if (!cfg.enabled) return;
|
|
277
|
+
const toolName = input.toolName ?? "unknown";
|
|
278
|
+
if (cfg.ignoreTools.includes(toolName)) return;
|
|
279
|
+
state.postInvocations += 1;
|
|
280
|
+
if (input.toolResult?.isError) {
|
|
281
|
+
const errorFingerprint = normalizeError(input.toolResult.content);
|
|
282
|
+
if (errorFingerprint && errorFingerprint === state.lastErrorFingerprint) {
|
|
283
|
+
state.repeatedErrorStreak += 1;
|
|
284
|
+
} else {
|
|
285
|
+
state.lastErrorFingerprint = errorFingerprint;
|
|
286
|
+
state.repeatedErrorStreak = errorFingerprint ? 1 : 0;
|
|
287
|
+
}
|
|
288
|
+
state.noDiffStreak = 0;
|
|
289
|
+
if (cfg.repeatedErrorBlockAfter > 0 && state.repeatedErrorStreak >= cfg.repeatedErrorBlockAfter && cfg.mode === "block") {
|
|
290
|
+
state.repeatedErrorBlocks += 1;
|
|
291
|
+
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.`;
|
|
292
|
+
}
|
|
293
|
+
if (state.repeatedErrorStreak >= cfg.repeatedErrorWarnAfter) {
|
|
294
|
+
state.repeatedErrorWarnings += 1;
|
|
295
|
+
api.metrics.counter("repeated_error_warnings");
|
|
296
|
+
return {
|
|
297
|
+
decision: "allow",
|
|
298
|
+
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.`
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
state.lastErrorFingerprint = null;
|
|
304
|
+
state.repeatedErrorStreak = 0;
|
|
305
|
+
if (!MUTATING_TOOLS.has(toolName)) return;
|
|
306
|
+
const diffFingerprint = gitDiffFingerprint(input.cwd ?? process.cwd());
|
|
307
|
+
if (diffFingerprint === null) return;
|
|
308
|
+
if (diffFingerprint === state.lastDiffFingerprint) {
|
|
309
|
+
state.noDiffStreak += 1;
|
|
310
|
+
} else {
|
|
311
|
+
state.lastDiffFingerprint = diffFingerprint;
|
|
312
|
+
state.noDiffStreak = 0;
|
|
313
|
+
}
|
|
314
|
+
if (cfg.noDiffBlockAfter > 0 && state.noDiffStreak >= cfg.noDiffBlockAfter && cfg.mode === "block") {
|
|
315
|
+
state.noDiffBlocks += 1;
|
|
316
|
+
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.`;
|
|
317
|
+
}
|
|
318
|
+
if (state.noDiffStreak >= cfg.noDiffWarnAfter) {
|
|
319
|
+
state.noDiffWarnings += 1;
|
|
320
|
+
api.metrics.counter("no_diff_warnings");
|
|
321
|
+
return {
|
|
322
|
+
decision: "allow",
|
|
323
|
+
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.`
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
};
|
|
328
|
+
const unregisterPre = api.registerHook("PreToolUse", "*", hook);
|
|
329
|
+
const unregisterPost = api.registerHook("PostToolUse", "*", postHook);
|
|
330
|
+
state.hookUnregister = () => {
|
|
331
|
+
unregisterPre();
|
|
332
|
+
unregisterPost();
|
|
333
|
+
};
|
|
169
334
|
api.tools.register({
|
|
170
335
|
name: "loop_breaker_status",
|
|
171
336
|
description: "Reports loop-breaker state: config, current repeat streak, and counters (warnings, blocks, oscillations).",
|
|
@@ -181,13 +346,26 @@ var plugin = {
|
|
|
181
346
|
warnAfter: cfg.warnAfter,
|
|
182
347
|
blockAfter: cfg.blockAfter,
|
|
183
348
|
oscillationWindow: cfg.oscillationWindow,
|
|
349
|
+
maxSteps: cfg.maxSteps,
|
|
350
|
+
noDiffWarnAfter: cfg.noDiffWarnAfter,
|
|
351
|
+
noDiffBlockAfter: cfg.noDiffBlockAfter,
|
|
352
|
+
repeatedErrorWarnAfter: cfg.repeatedErrorWarnAfter,
|
|
353
|
+
repeatedErrorBlockAfter: cfg.repeatedErrorBlockAfter,
|
|
184
354
|
ignoreTools: cfg.ignoreTools,
|
|
185
355
|
currentStreak: state.streak,
|
|
356
|
+
noDiffStreak: state.noDiffStreak,
|
|
357
|
+
repeatedErrorStreak: state.repeatedErrorStreak,
|
|
186
358
|
counters: {
|
|
187
359
|
invocations: state.invocations,
|
|
360
|
+
postInvocations: state.postInvocations,
|
|
188
361
|
warnings: state.warnings,
|
|
189
362
|
blocks: state.blocks,
|
|
190
|
-
oscillationsDetected: state.oscillationsDetected
|
|
363
|
+
oscillationsDetected: state.oscillationsDetected,
|
|
364
|
+
stepBudgetBlocks: state.stepBudgetBlocks,
|
|
365
|
+
noDiffWarnings: state.noDiffWarnings,
|
|
366
|
+
noDiffBlocks: state.noDiffBlocks,
|
|
367
|
+
repeatedErrorWarnings: state.repeatedErrorWarnings,
|
|
368
|
+
repeatedErrorBlocks: state.repeatedErrorBlocks
|
|
191
369
|
}
|
|
192
370
|
};
|
|
193
371
|
}
|
|
@@ -210,28 +388,51 @@ var plugin = {
|
|
|
210
388
|
}
|
|
211
389
|
const final = {
|
|
212
390
|
invocations: state.invocations,
|
|
391
|
+
postInvocations: state.postInvocations,
|
|
213
392
|
warnings: state.warnings,
|
|
214
393
|
blocks: state.blocks,
|
|
215
|
-
oscillationsDetected: state.oscillationsDetected
|
|
394
|
+
oscillationsDetected: state.oscillationsDetected,
|
|
395
|
+
stepBudgetBlocks: state.stepBudgetBlocks,
|
|
396
|
+
noDiffWarnings: state.noDiffWarnings,
|
|
397
|
+
noDiffBlocks: state.noDiffBlocks,
|
|
398
|
+
repeatedErrorWarnings: state.repeatedErrorWarnings,
|
|
399
|
+
repeatedErrorBlocks: state.repeatedErrorBlocks
|
|
216
400
|
};
|
|
217
401
|
state.lastFingerprint = null;
|
|
218
402
|
state.streak = 0;
|
|
219
403
|
state.recent = [];
|
|
404
|
+
state.pendingBlockReason = null;
|
|
405
|
+
state.lastDiffFingerprint = null;
|
|
406
|
+
state.noDiffStreak = 0;
|
|
407
|
+
state.lastErrorFingerprint = null;
|
|
408
|
+
state.repeatedErrorStreak = 0;
|
|
220
409
|
state.invocations = 0;
|
|
410
|
+
state.postInvocations = 0;
|
|
221
411
|
state.warnings = 0;
|
|
222
412
|
state.blocks = 0;
|
|
223
413
|
state.oscillationsDetected = 0;
|
|
414
|
+
state.stepBudgetBlocks = 0;
|
|
415
|
+
state.noDiffWarnings = 0;
|
|
416
|
+
state.noDiffBlocks = 0;
|
|
417
|
+
state.repeatedErrorWarnings = 0;
|
|
418
|
+
state.repeatedErrorBlocks = 0;
|
|
224
419
|
api.log.info("loop-breaker: teardown complete", { final });
|
|
225
420
|
},
|
|
226
421
|
async health() {
|
|
227
422
|
return {
|
|
228
423
|
ok: true,
|
|
229
|
-
message: `loop-breaker: ${state.invocations} call(s) observed, ${state.warnings} warning(s), ${state.blocks} block(s), ${state.oscillationsDetected} oscillation(s)`,
|
|
424
|
+
message: `loop-breaker: ${state.invocations} call(s) observed, ${state.warnings + state.noDiffWarnings + state.repeatedErrorWarnings} warning(s), ${state.blocks} block(s), ${state.oscillationsDetected} oscillation(s)`,
|
|
230
425
|
counters: {
|
|
231
426
|
invocations: state.invocations,
|
|
427
|
+
postInvocations: state.postInvocations,
|
|
232
428
|
warnings: state.warnings,
|
|
233
429
|
blocks: state.blocks,
|
|
234
|
-
oscillationsDetected: state.oscillationsDetected
|
|
430
|
+
oscillationsDetected: state.oscillationsDetected,
|
|
431
|
+
stepBudgetBlocks: state.stepBudgetBlocks,
|
|
432
|
+
noDiffWarnings: state.noDiffWarnings,
|
|
433
|
+
noDiffBlocks: state.noDiffBlocks,
|
|
434
|
+
repeatedErrorWarnings: state.repeatedErrorWarnings,
|
|
435
|
+
repeatedErrorBlocks: state.repeatedErrorBlocks
|
|
235
436
|
}
|
|
236
437
|
};
|
|
237
438
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* migration-planner plugin — helps plan dependency or framework migrations.
|
|
5
|
+
*
|
|
6
|
+
* The plugin reads a package CHANGELOG (project root or node_modules) and
|
|
7
|
+
* produces a migration checklist with breaking changes and recommended steps
|
|
8
|
+
* between two versions. If no changelog is available it returns a generic
|
|
9
|
+
* migration guide.
|
|
10
|
+
*
|
|
11
|
+
* Tools registered:
|
|
12
|
+
* - migration_plan — produce a migration checklist for a package/version range
|
|
13
|
+
* - migration_status — report plugin state and counters
|
|
14
|
+
*
|
|
15
|
+
* Config (`config.extensions['migration-planner']`):
|
|
16
|
+
*
|
|
17
|
+
* ```jsonc
|
|
18
|
+
* {
|
|
19
|
+
* "enabled": true,
|
|
20
|
+
* "changelogPaths": ["CHANGELOG.md"],
|
|
21
|
+
* "maxChars": 100_000
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
declare const plugin: Plugin;
|
|
29
|
+
|
|
30
|
+
export { plugin as default };
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { isAbsolute, resolve, relative } from 'path';
|
|
3
|
+
|
|
4
|
+
// src/migration-planner/index.ts
|
|
5
|
+
var API_VERSION = "^0.1.10";
|
|
6
|
+
var state = {
|
|
7
|
+
plansGenerated: 0,
|
|
8
|
+
statusQueries: 0,
|
|
9
|
+
fallbackCount: 0,
|
|
10
|
+
lastPlan: null,
|
|
11
|
+
hookUnregister: null
|
|
12
|
+
};
|
|
13
|
+
var DEFAULTS = {
|
|
14
|
+
enabled: true,
|
|
15
|
+
changelogPaths: ["CHANGELOG.md"],
|
|
16
|
+
maxChars: 1e5
|
|
17
|
+
};
|
|
18
|
+
function readConfig(raw) {
|
|
19
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
20
|
+
const r = raw;
|
|
21
|
+
return {
|
|
22
|
+
enabled: r["enabled"] !== false,
|
|
23
|
+
changelogPaths: Array.isArray(r["changelogPaths"]) ? r["changelogPaths"].filter((x) => typeof x === "string") : DEFAULTS.changelogPaths,
|
|
24
|
+
maxChars: typeof r["maxChars"] === "number" && r["maxChars"] >= 1e3 && r["maxChars"] <= 1e6 ? r["maxChars"] : DEFAULTS.maxChars
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function withinProject(p) {
|
|
28
|
+
if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
|
|
29
|
+
const root = process.cwd();
|
|
30
|
+
const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
|
|
31
|
+
const rel = relative(root, resolved);
|
|
32
|
+
if (rel === "" || rel === ".") return true;
|
|
33
|
+
if (rel.startsWith("..")) return false;
|
|
34
|
+
if (isAbsolute(rel)) return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
function normalizeVersion(v) {
|
|
38
|
+
return v.replace(/^v/i, "").trim();
|
|
39
|
+
}
|
|
40
|
+
function readChangelog(packageName, cfg) {
|
|
41
|
+
const candidates = cfg.changelogPaths.map((p) => p.replace(/<package>/g, packageName));
|
|
42
|
+
candidates.push(`node_modules/${packageName}/CHANGELOG.md`);
|
|
43
|
+
candidates.push(`node_modules/${packageName}/changelog.md`);
|
|
44
|
+
for (const candidate of candidates) {
|
|
45
|
+
if (!withinProject(candidate)) continue;
|
|
46
|
+
if (existsSync(candidate)) {
|
|
47
|
+
try {
|
|
48
|
+
const content = readFileSync(candidate, "utf-8");
|
|
49
|
+
return { source: candidate, content: content.slice(0, cfg.maxChars) };
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function extractVersionSections(changelog, fromVersion, toVersion) {
|
|
57
|
+
const fromNv = normalizeVersion(fromVersion);
|
|
58
|
+
const toNv = normalizeVersion(toVersion);
|
|
59
|
+
const sections = [];
|
|
60
|
+
let current = null;
|
|
61
|
+
for (const line of changelog.split(/\r?\n/)) {
|
|
62
|
+
const match = line.match(/^#{2,3}\s+(\[?v?(\d+\.\d+\.\d+[^[\]\s]*)\]?)\s*(.*)$/);
|
|
63
|
+
if (match) {
|
|
64
|
+
if (current) sections.push(current);
|
|
65
|
+
current = { version: normalizeVersion(match[2]), header: match[1], body: "" };
|
|
66
|
+
} else if (current) {
|
|
67
|
+
current.body += `${line}
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (current) sections.push(current);
|
|
72
|
+
if (sections.length === 0) return [changelog];
|
|
73
|
+
const relevant = [];
|
|
74
|
+
for (const section of sections) {
|
|
75
|
+
if (section.version === toNv) {
|
|
76
|
+
relevant.push(`## ${section.header}
|
|
77
|
+
${section.body}`);
|
|
78
|
+
} else if (section.version === fromNv) {
|
|
79
|
+
break;
|
|
80
|
+
} else if (relevant.length > 0) {
|
|
81
|
+
relevant.push(`## ${section.header}
|
|
82
|
+
${section.body}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return relevant.length > 0 ? relevant : [changelog];
|
|
86
|
+
}
|
|
87
|
+
function extractBreakingChanges(sectionText) {
|
|
88
|
+
const breaking = [];
|
|
89
|
+
let inBreakingSection = false;
|
|
90
|
+
for (const rawLine of sectionText.split(/\r?\n/)) {
|
|
91
|
+
const line = rawLine.trim();
|
|
92
|
+
if (!line) {
|
|
93
|
+
inBreakingSection = false;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (/^#{3,4}\s+(?:BREAKING\s+CHANGES?|Breaking\s+Changes?|Breaking)/i.test(line)) {
|
|
97
|
+
inBreakingSection = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (/^#{1,4}\s+/.test(line)) {
|
|
101
|
+
inBreakingSection = false;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (inBreakingSection) {
|
|
105
|
+
const item = line.replace(/^[-*]\s+/, "").replace(/^\d+\.\s+/, "");
|
|
106
|
+
if (item) breaking.push(item);
|
|
107
|
+
} else if (/^\s*[-*]\s+.*(?:BREAKING|breaking|removed|deprecated|no longer supported)/i.test(rawLine)) {
|
|
108
|
+
breaking.push(line.replace(/^[-*]\s+/, ""));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return breaking;
|
|
112
|
+
}
|
|
113
|
+
function extractRecommendedSteps(sectionText) {
|
|
114
|
+
const steps = [];
|
|
115
|
+
let inMigrationSection = false;
|
|
116
|
+
for (const rawLine of sectionText.split(/\r?\n/)) {
|
|
117
|
+
const line = rawLine.trim();
|
|
118
|
+
if (!line) {
|
|
119
|
+
inMigrationSection = false;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (/^#{3,4}\s+(?:Migration|Upgrade|How to|Steps|Recommended)/i.test(line)) {
|
|
123
|
+
inMigrationSection = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (/^#{1,4}\s+/.test(line)) {
|
|
127
|
+
inMigrationSection = false;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (inMigrationSection) {
|
|
131
|
+
const item = line.replace(/^[-*]\s+/, "").replace(/^\d+\.\s+/, "");
|
|
132
|
+
if (item) steps.push(item);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (steps.length === 0) {
|
|
136
|
+
steps.push("Review the changelog for deprecated APIs.");
|
|
137
|
+
steps.push("Update imports and call sites to new API signatures.");
|
|
138
|
+
steps.push("Run the test suite and fix regressions.");
|
|
139
|
+
steps.push("Verify type-checking passes with the new version.");
|
|
140
|
+
}
|
|
141
|
+
return steps;
|
|
142
|
+
}
|
|
143
|
+
function buildGenericGuide(packageName, fromVersion, toVersion, scope) {
|
|
144
|
+
return {
|
|
145
|
+
breakingChanges: [
|
|
146
|
+
`No changelog found for ${packageName}. Unknown breaking changes between ${fromVersion} and ${toVersion}.`
|
|
147
|
+
],
|
|
148
|
+
recommendedSteps: [
|
|
149
|
+
`Visit ${packageName} release notes or GitHub releases for ${toVersion}.`,
|
|
150
|
+
scope ? `Review ${scope} usage of ${packageName} for API changes.` : `Search the codebase for direct ${packageName} usage.`,
|
|
151
|
+
`Update ${packageName} from ${fromVersion} to ${toVersion} in package.json.`,
|
|
152
|
+
"Run install and the full test suite.",
|
|
153
|
+
"Fix type errors and runtime regressions."
|
|
154
|
+
]
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
var plugin = {
|
|
158
|
+
name: "migration-planner",
|
|
159
|
+
version: "0.1.0",
|
|
160
|
+
description: "Helps plan dependency or framework migrations by reading changelogs and producing checklists",
|
|
161
|
+
apiVersion: API_VERSION,
|
|
162
|
+
capabilities: { tools: true, hooks: true },
|
|
163
|
+
defaultConfig: { ...DEFAULTS },
|
|
164
|
+
configSchema: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
enabled: {
|
|
168
|
+
type: "boolean",
|
|
169
|
+
default: true,
|
|
170
|
+
description: "Master switch."
|
|
171
|
+
},
|
|
172
|
+
changelogPaths: {
|
|
173
|
+
type: "array",
|
|
174
|
+
items: { type: "string" },
|
|
175
|
+
default: ["CHANGELOG.md"],
|
|
176
|
+
description: "Changelog paths to try; <package> is replaced with the package name."
|
|
177
|
+
},
|
|
178
|
+
maxChars: {
|
|
179
|
+
type: "number",
|
|
180
|
+
minimum: 1e3,
|
|
181
|
+
maximum: 1e6,
|
|
182
|
+
default: 1e5,
|
|
183
|
+
description: "Maximum changelog characters to scan."
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
setup(api) {
|
|
188
|
+
state.plansGenerated = 0;
|
|
189
|
+
state.statusQueries = 0;
|
|
190
|
+
state.fallbackCount = 0;
|
|
191
|
+
state.lastPlan = null;
|
|
192
|
+
state.hookUnregister = null;
|
|
193
|
+
const cfg = readConfig(api.config.extensions?.["migration-planner"]);
|
|
194
|
+
const hook = (input) => {
|
|
195
|
+
if (!cfg.enabled) return;
|
|
196
|
+
if (input.toolResult?.isError) return;
|
|
197
|
+
const inp = input.toolInput ?? {};
|
|
198
|
+
const path = inp["path"];
|
|
199
|
+
if (!path) return;
|
|
200
|
+
const basename = path.split(/[/\\]/).pop() ?? "";
|
|
201
|
+
if (!/^(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/i.test(
|
|
202
|
+
basename
|
|
203
|
+
)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
additionalContext: `Manifest file ${basename} changed. Consider running migration_plan if a dependency version was updated.`
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
|
|
211
|
+
api.tools.register({
|
|
212
|
+
name: "migration_plan",
|
|
213
|
+
description: "Read a package CHANGELOG and produce a migration checklist with breaking changes and recommended steps between two versions.",
|
|
214
|
+
inputSchema: {
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {
|
|
217
|
+
packageName: {
|
|
218
|
+
type: "string",
|
|
219
|
+
description: "Name of the npm package or framework."
|
|
220
|
+
},
|
|
221
|
+
fromVersion: {
|
|
222
|
+
type: "string",
|
|
223
|
+
description: 'Current version, e.g. "1.2.3".'
|
|
224
|
+
},
|
|
225
|
+
toVersion: {
|
|
226
|
+
type: "string",
|
|
227
|
+
description: 'Target version, e.g. "2.0.0".'
|
|
228
|
+
},
|
|
229
|
+
scope: {
|
|
230
|
+
type: "string",
|
|
231
|
+
description: "Optional scope describing which parts of the project use the package."
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
required: ["packageName", "fromVersion", "toVersion"]
|
|
235
|
+
},
|
|
236
|
+
permission: "auto",
|
|
237
|
+
category: "Planning",
|
|
238
|
+
mutating: false,
|
|
239
|
+
async execute(input) {
|
|
240
|
+
if (!cfg.enabled) return { ok: false, error: "migration-planner is disabled" };
|
|
241
|
+
const packageName = String(input.packageName ?? "").trim();
|
|
242
|
+
const fromVersion = String(input.fromVersion ?? "").trim();
|
|
243
|
+
const toVersion = String(input.toVersion ?? "").trim();
|
|
244
|
+
if (!packageName || !fromVersion || !toVersion) {
|
|
245
|
+
return { ok: false, error: "packageName, fromVersion, and toVersion are required" };
|
|
246
|
+
}
|
|
247
|
+
const changelog = readChangelog(packageName, cfg);
|
|
248
|
+
let breakingChanges;
|
|
249
|
+
let recommendedSteps;
|
|
250
|
+
let source;
|
|
251
|
+
if (changelog) {
|
|
252
|
+
source = changelog.source;
|
|
253
|
+
const sections = extractVersionSections(changelog.content, fromVersion, toVersion);
|
|
254
|
+
const combined = sections.join("\n\n");
|
|
255
|
+
breakingChanges = extractBreakingChanges(combined);
|
|
256
|
+
recommendedSteps = extractRecommendedSteps(combined);
|
|
257
|
+
} else {
|
|
258
|
+
state.fallbackCount += 1;
|
|
259
|
+
source = null;
|
|
260
|
+
const guide = buildGenericGuide(packageName, fromVersion, toVersion, input.scope);
|
|
261
|
+
breakingChanges = guide.breakingChanges;
|
|
262
|
+
recommendedSteps = guide.recommendedSteps;
|
|
263
|
+
}
|
|
264
|
+
state.plansGenerated += 1;
|
|
265
|
+
state.lastPlan = {
|
|
266
|
+
packageName,
|
|
267
|
+
fromVersion,
|
|
268
|
+
toVersion,
|
|
269
|
+
scope: input.scope,
|
|
270
|
+
breakingChanges,
|
|
271
|
+
recommendedSteps,
|
|
272
|
+
changelogSource: source
|
|
273
|
+
};
|
|
274
|
+
return {
|
|
275
|
+
ok: true,
|
|
276
|
+
packageName,
|
|
277
|
+
fromVersion,
|
|
278
|
+
toVersion,
|
|
279
|
+
scope: input.scope,
|
|
280
|
+
changelogSource: source,
|
|
281
|
+
fallback: source === null,
|
|
282
|
+
breakingChanges,
|
|
283
|
+
recommendedSteps
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
api.tools.register({
|
|
288
|
+
name: "migration_status",
|
|
289
|
+
description: "Reports migration-planner state: counters, config, and last plan.",
|
|
290
|
+
inputSchema: { type: "object", properties: {} },
|
|
291
|
+
permission: "auto",
|
|
292
|
+
category: "Diagnostics",
|
|
293
|
+
mutating: false,
|
|
294
|
+
async execute() {
|
|
295
|
+
state.statusQueries += 1;
|
|
296
|
+
return {
|
|
297
|
+
ok: true,
|
|
298
|
+
enabled: cfg.enabled,
|
|
299
|
+
changelogPaths: cfg.changelogPaths,
|
|
300
|
+
maxChars: cfg.maxChars,
|
|
301
|
+
counters: {
|
|
302
|
+
plansGenerated: state.plansGenerated,
|
|
303
|
+
statusQueries: state.statusQueries,
|
|
304
|
+
fallbackCount: state.fallbackCount
|
|
305
|
+
},
|
|
306
|
+
lastPlan: state.lastPlan
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
api.log.info("migration-planner plugin loaded", {
|
|
311
|
+
version: "0.1.0",
|
|
312
|
+
changelogPaths: cfg.changelogPaths
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
teardown(api) {
|
|
316
|
+
if (state.hookUnregister) {
|
|
317
|
+
try {
|
|
318
|
+
state.hookUnregister();
|
|
319
|
+
} catch {
|
|
320
|
+
}
|
|
321
|
+
state.hookUnregister = null;
|
|
322
|
+
}
|
|
323
|
+
const final = {
|
|
324
|
+
plansGenerated: state.plansGenerated,
|
|
325
|
+
statusQueries: state.statusQueries,
|
|
326
|
+
fallbackCount: state.fallbackCount
|
|
327
|
+
};
|
|
328
|
+
state.plansGenerated = 0;
|
|
329
|
+
state.statusQueries = 0;
|
|
330
|
+
state.fallbackCount = 0;
|
|
331
|
+
state.lastPlan = null;
|
|
332
|
+
api.log.info("migration-planner: teardown complete", { final });
|
|
333
|
+
},
|
|
334
|
+
async health() {
|
|
335
|
+
return {
|
|
336
|
+
ok: true,
|
|
337
|
+
message: `migration-planner: ${state.plansGenerated} plan(s), ${state.fallbackCount} fallback(s)`,
|
|
338
|
+
counters: {
|
|
339
|
+
plansGenerated: state.plansGenerated,
|
|
340
|
+
statusQueries: state.statusQueries,
|
|
341
|
+
fallbackCount: state.fallbackCount
|
|
342
|
+
},
|
|
343
|
+
lastPlan: state.lastPlan
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
var migration_planner_default = plugin;
|
|
348
|
+
|
|
349
|
+
export { migration_planner_default as default };
|