@wrongstack/plugins 0.281.3 → 0.282.1
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 +30 -4
- 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-escalate.d.ts +1 -1
- package/dist/auto-i18n-extractor.d.ts +36 -0
- package/dist/auto-i18n-extractor.js +335 -0
- package/dist/branch-guard.d.ts +6 -5
- package/dist/branch-guard.js +54 -4
- 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/context-pins.js +4 -4
- package/dist/cost-tracker.d.ts +1 -1
- package/dist/cost-tracker.js +58 -19
- package/dist/cron.js +18 -8
- 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 +99 -18
- package/dist/import-organizer.js +73 -14
- package/dist/index.d.ts +27 -0
- package/dist/index.js +11205 -2106
- 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 +138 -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/model-router.js
CHANGED
|
@@ -107,6 +107,11 @@ var plugin = {
|
|
|
107
107
|
}
|
|
108
108
|
const cfg = readConfig(api.config.extensions?.["model-router"]);
|
|
109
109
|
if (cfg.enabled && cfg.rules.length > 0) {
|
|
110
|
+
api.emitCustom?.("provider.wrap:loaded", {
|
|
111
|
+
plugin: "model-router",
|
|
112
|
+
kind: "routing",
|
|
113
|
+
wraps: ["request"]
|
|
114
|
+
});
|
|
110
115
|
state.extensionUnregister = api.extensions.register({
|
|
111
116
|
name: "model-router",
|
|
112
117
|
owner: "model-router",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* performance-regression-gate plugin — compares benchmark results to detect
|
|
5
|
+
* performance regressions.
|
|
6
|
+
*
|
|
7
|
+
* Tool registered:
|
|
8
|
+
* - perf_regression_status : Reads `bench-results.json` in the project root
|
|
9
|
+
* (or a supplied path) and reports benchmarks whose execution time (`mean`)
|
|
10
|
+
* increased beyond the configured threshold.
|
|
11
|
+
*
|
|
12
|
+
* The tool supports two comparison modes:
|
|
13
|
+
* 1. Internal pairing: when only `resultsPath` is supplied, benchmarks named
|
|
14
|
+
* with `(old)` and `(new)` suffixes are paired. This matches Vitest bench
|
|
15
|
+
* output comparing an old implementation against a new one.
|
|
16
|
+
* 2. Cross-file comparison: when `baselinePath` is also supplied, benchmarks
|
|
17
|
+
* are matched by name across the two files.
|
|
18
|
+
*
|
|
19
|
+
* Config (`config.extensions['performance-regression-gate']`):
|
|
20
|
+
*
|
|
21
|
+
* ```jsonc
|
|
22
|
+
* {
|
|
23
|
+
* "enabled": true,
|
|
24
|
+
* "thresholdPercent": 10
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
declare const plugin: Plugin;
|
|
32
|
+
|
|
33
|
+
export { plugin as default };
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { resolve, isAbsolute, relative } from 'path';
|
|
3
|
+
|
|
4
|
+
// src/performance-regression-gate/index.ts
|
|
5
|
+
var API_VERSION = "^0.1.10";
|
|
6
|
+
var state = {
|
|
7
|
+
invocationCount: 0,
|
|
8
|
+
comparisonCount: 0,
|
|
9
|
+
regressionCount: 0,
|
|
10
|
+
missingResultsCount: 0,
|
|
11
|
+
errorCount: 0,
|
|
12
|
+
lastResult: null
|
|
13
|
+
};
|
|
14
|
+
var DEFAULTS = {
|
|
15
|
+
enabled: true,
|
|
16
|
+
thresholdPercent: 10
|
|
17
|
+
};
|
|
18
|
+
function readConfig(raw) {
|
|
19
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
20
|
+
const r = raw;
|
|
21
|
+
const threshold = typeof r["thresholdPercent"] === "number" && r["thresholdPercent"] >= 0 && r["thresholdPercent"] <= 1e3 ? r["thresholdPercent"] : DEFAULTS.thresholdPercent;
|
|
22
|
+
return {
|
|
23
|
+
enabled: r["enabled"] !== false,
|
|
24
|
+
thresholdPercent: threshold
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function withinProject(p, cwd = process.cwd()) {
|
|
28
|
+
if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
|
|
29
|
+
const root = resolve(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 resolveProjectPath(rawPath, cwd = process.cwd()) {
|
|
38
|
+
if (typeof rawPath !== "string" || rawPath.length === 0) return null;
|
|
39
|
+
const root = resolve(cwd);
|
|
40
|
+
const resolved = isAbsolute(rawPath) ? resolve(rawPath) : resolve(root, rawPath);
|
|
41
|
+
if (!withinProject(resolved, cwd)) return null;
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
function isValidNumber(n) {
|
|
45
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0;
|
|
46
|
+
}
|
|
47
|
+
function flattenResults(results) {
|
|
48
|
+
const flat = [];
|
|
49
|
+
for (const file of results.files ?? []) {
|
|
50
|
+
for (const group of file.groups ?? []) {
|
|
51
|
+
for (const bench of group.benchmarks ?? []) {
|
|
52
|
+
if (!bench || typeof bench.name !== "string") continue;
|
|
53
|
+
const mean = isValidNumber(bench.mean) ? bench.mean : isValidNumber(bench.period) ? bench.period : null;
|
|
54
|
+
if (mean === null) continue;
|
|
55
|
+
flat.push({
|
|
56
|
+
key: `${group.fullName} > ${bench.name}`,
|
|
57
|
+
file: file.filepath ?? "",
|
|
58
|
+
group: group.fullName ?? "",
|
|
59
|
+
name: bench.name,
|
|
60
|
+
mean,
|
|
61
|
+
hz: isValidNumber(bench.hz) ? bench.hz : null
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return flat;
|
|
67
|
+
}
|
|
68
|
+
function loadResults(path) {
|
|
69
|
+
if (!path || !existsSync(path)) return null;
|
|
70
|
+
try {
|
|
71
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
72
|
+
return raw;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function stripVariantSuffix(name) {
|
|
78
|
+
const trimmed = name.trim();
|
|
79
|
+
const oldMatch = /\(old\)\s*$/i.exec(trimmed);
|
|
80
|
+
if (oldMatch) {
|
|
81
|
+
return { base: trimmed.slice(0, oldMatch.index).trim(), variant: "old" };
|
|
82
|
+
}
|
|
83
|
+
const newMatch = /\(new\)\s*$/i.exec(trimmed);
|
|
84
|
+
if (newMatch) {
|
|
85
|
+
return { base: trimmed.slice(0, newMatch.index).trim(), variant: "new" };
|
|
86
|
+
}
|
|
87
|
+
return { base: trimmed, variant: null };
|
|
88
|
+
}
|
|
89
|
+
function pairInternal(benchmarks) {
|
|
90
|
+
const byBase = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const bench of benchmarks) {
|
|
92
|
+
const { base, variant } = stripVariantSuffix(bench.name);
|
|
93
|
+
if (!variant) continue;
|
|
94
|
+
const groupBase = `${bench.group} > ${base}`;
|
|
95
|
+
const entry = byBase.get(groupBase) ?? {};
|
|
96
|
+
if (variant === "old") entry.old = bench;
|
|
97
|
+
if (variant === "new") entry.new = bench;
|
|
98
|
+
byBase.set(groupBase, entry);
|
|
99
|
+
}
|
|
100
|
+
const pairs = [];
|
|
101
|
+
for (const entry of byBase.values()) {
|
|
102
|
+
if (entry.old && entry.new) {
|
|
103
|
+
pairs.push({ old: entry.old, new: entry.new });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return pairs;
|
|
107
|
+
}
|
|
108
|
+
function pairCrossFile(baseline, current) {
|
|
109
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const bench of baseline) {
|
|
111
|
+
byKey.set(bench.key, bench);
|
|
112
|
+
}
|
|
113
|
+
const pairs = [];
|
|
114
|
+
for (const bench of current) {
|
|
115
|
+
const base = stripVariantSuffix(bench.name).base;
|
|
116
|
+
const lookupKey = `${bench.group} > ${base}`;
|
|
117
|
+
const old = byKey.get(lookupKey) ?? byKey.get(bench.key);
|
|
118
|
+
if (old) {
|
|
119
|
+
pairs.push({ old, new: bench });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return pairs;
|
|
123
|
+
}
|
|
124
|
+
function comparePairs(pairs, thresholdPercent) {
|
|
125
|
+
const regressions = [];
|
|
126
|
+
const thresholdFactor = 1 + thresholdPercent / 100;
|
|
127
|
+
for (const { old, new: current } of pairs) {
|
|
128
|
+
if (current.mean > old.mean * thresholdFactor) {
|
|
129
|
+
const increasePercent = (current.mean - old.mean) / old.mean * 100;
|
|
130
|
+
regressions.push({
|
|
131
|
+
benchmark: current.key,
|
|
132
|
+
metric: "mean",
|
|
133
|
+
oldValue: old.mean,
|
|
134
|
+
newValue: current.mean,
|
|
135
|
+
increasePercent: Math.round(increasePercent * 100) / 100
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return regressions;
|
|
140
|
+
}
|
|
141
|
+
var plugin = {
|
|
142
|
+
name: "performance-regression-gate",
|
|
143
|
+
version: "0.1.0",
|
|
144
|
+
description: "Compares benchmark results to detect performance regressions and reports metrics that increased beyond the configured threshold",
|
|
145
|
+
apiVersion: API_VERSION,
|
|
146
|
+
capabilities: { tools: true },
|
|
147
|
+
defaultConfig: { ...DEFAULTS },
|
|
148
|
+
configSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
enabled: {
|
|
152
|
+
type: "boolean",
|
|
153
|
+
default: true,
|
|
154
|
+
description: "Master switch."
|
|
155
|
+
},
|
|
156
|
+
thresholdPercent: {
|
|
157
|
+
type: "number",
|
|
158
|
+
minimum: 0,
|
|
159
|
+
maximum: 1e3,
|
|
160
|
+
default: 10,
|
|
161
|
+
description: "Percentage increase in mean execution time that counts as a regression."
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
setup(api) {
|
|
166
|
+
state.invocationCount = 0;
|
|
167
|
+
state.comparisonCount = 0;
|
|
168
|
+
state.regressionCount = 0;
|
|
169
|
+
state.missingResultsCount = 0;
|
|
170
|
+
state.errorCount = 0;
|
|
171
|
+
state.lastResult = null;
|
|
172
|
+
const cfg = readConfig(api.config.extensions?.["performance-regression-gate"]);
|
|
173
|
+
api.tools.register({
|
|
174
|
+
name: "perf_regression_status",
|
|
175
|
+
description: "Reads bench-results.json and reports performance regressions. Without baselinePath, pairs benchmarks named with (old) and (new) suffixes. With baselinePath, compares matching benchmarks across the two files.",
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: {
|
|
179
|
+
resultsPath: {
|
|
180
|
+
type: "string",
|
|
181
|
+
default: "bench-results.json",
|
|
182
|
+
description: "Path to current benchmark results (JSON)."
|
|
183
|
+
},
|
|
184
|
+
baselinePath: {
|
|
185
|
+
type: "string",
|
|
186
|
+
description: "Optional path to baseline/previous results. If omitted, (old)/(new) pairs within resultsPath are compared."
|
|
187
|
+
},
|
|
188
|
+
thresholdPercent: {
|
|
189
|
+
type: "number",
|
|
190
|
+
description: "Override the configured threshold percentage."
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
permission: "auto",
|
|
195
|
+
category: "Diagnostics",
|
|
196
|
+
mutating: false,
|
|
197
|
+
async execute(input) {
|
|
198
|
+
if (!cfg.enabled) {
|
|
199
|
+
return { ok: false, error: "performance-regression-gate is disabled" };
|
|
200
|
+
}
|
|
201
|
+
state.invocationCount += 1;
|
|
202
|
+
const threshold = typeof input.thresholdPercent === "number" && input.thresholdPercent >= 0 && input.thresholdPercent <= 1e3 ? input.thresholdPercent : cfg.thresholdPercent;
|
|
203
|
+
const resultsPath = resolveProjectPath(input.resultsPath || "bench-results.json") ?? "";
|
|
204
|
+
if (!resultsPath) {
|
|
205
|
+
state.errorCount += 1;
|
|
206
|
+
return { ok: false, error: "invalid results path (must be inside project)" };
|
|
207
|
+
}
|
|
208
|
+
const results = loadResults(resultsPath);
|
|
209
|
+
if (!results) {
|
|
210
|
+
state.missingResultsCount += 1;
|
|
211
|
+
return {
|
|
212
|
+
ok: true,
|
|
213
|
+
hasResults: false,
|
|
214
|
+
thresholdPercent: threshold,
|
|
215
|
+
comparisons: 0,
|
|
216
|
+
regressions: [],
|
|
217
|
+
message: `No benchmark results found at ${input.resultsPath || "bench-results.json"}.`
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const current = flattenResults(results);
|
|
221
|
+
if (current.length === 0) {
|
|
222
|
+
state.missingResultsCount += 1;
|
|
223
|
+
return {
|
|
224
|
+
ok: true,
|
|
225
|
+
hasResults: false,
|
|
226
|
+
thresholdPercent: threshold,
|
|
227
|
+
comparisons: 0,
|
|
228
|
+
regressions: [],
|
|
229
|
+
message: "No benchmarks with valid mean/period values were found."
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
let pairs;
|
|
233
|
+
if (input.baselinePath) {
|
|
234
|
+
const baselineResolved = resolveProjectPath(input.baselinePath) ?? "";
|
|
235
|
+
if (!baselineResolved) {
|
|
236
|
+
state.errorCount += 1;
|
|
237
|
+
return { ok: false, error: "invalid baseline path (must be inside project)" };
|
|
238
|
+
}
|
|
239
|
+
const baselineResults = loadResults(baselineResolved);
|
|
240
|
+
if (!baselineResults) {
|
|
241
|
+
state.errorCount += 1;
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
error: `Could not read baseline results at ${input.baselinePath}.`
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const baseline = flattenResults(baselineResults);
|
|
248
|
+
pairs = pairCrossFile(baseline, current);
|
|
249
|
+
} else {
|
|
250
|
+
pairs = pairInternal(current);
|
|
251
|
+
}
|
|
252
|
+
const regressions = comparePairs(pairs, threshold);
|
|
253
|
+
state.comparisonCount += pairs.length;
|
|
254
|
+
state.regressionCount += regressions.length;
|
|
255
|
+
state.lastResult = {
|
|
256
|
+
comparisons: pairs.length,
|
|
257
|
+
regressions: regressions.length,
|
|
258
|
+
thresholdPercent: threshold,
|
|
259
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
260
|
+
};
|
|
261
|
+
api.metrics.counter("comparisons", pairs.length);
|
|
262
|
+
if (regressions.length > 0) {
|
|
263
|
+
api.metrics.counter("regressions", regressions.length);
|
|
264
|
+
}
|
|
265
|
+
const regression = regressions.length > 0;
|
|
266
|
+
return {
|
|
267
|
+
ok: true,
|
|
268
|
+
hasResults: true,
|
|
269
|
+
regression,
|
|
270
|
+
thresholdPercent: threshold,
|
|
271
|
+
comparisons: pairs.length,
|
|
272
|
+
regressions,
|
|
273
|
+
message: regression ? `Detected ${regressions.length} regression(s) above ${threshold}% threshold.` : `No regressions detected across ${pairs.length} benchmark comparison(s) (threshold ${threshold}%).`
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
api.log.info("performance-regression-gate plugin loaded", {
|
|
278
|
+
version: "0.1.0",
|
|
279
|
+
thresholdPercent: cfg.thresholdPercent
|
|
280
|
+
});
|
|
281
|
+
},
|
|
282
|
+
teardown(api) {
|
|
283
|
+
const final = {
|
|
284
|
+
invocations: state.invocationCount,
|
|
285
|
+
comparisons: state.comparisonCount,
|
|
286
|
+
regressions: state.regressionCount,
|
|
287
|
+
missingResults: state.missingResultsCount,
|
|
288
|
+
errors: state.errorCount
|
|
289
|
+
};
|
|
290
|
+
state.invocationCount = 0;
|
|
291
|
+
state.comparisonCount = 0;
|
|
292
|
+
state.regressionCount = 0;
|
|
293
|
+
state.missingResultsCount = 0;
|
|
294
|
+
state.errorCount = 0;
|
|
295
|
+
state.lastResult = null;
|
|
296
|
+
api.log.info("performance-regression-gate: teardown complete", { final });
|
|
297
|
+
},
|
|
298
|
+
async health() {
|
|
299
|
+
return {
|
|
300
|
+
ok: state.errorCount === 0,
|
|
301
|
+
message: state.lastResult ? `performance-regression-gate: ${state.lastResult.regressions} regression(s) across ${state.lastResult.comparisons} comparison(s) (threshold ${state.lastResult.thresholdPercent}%)` : `performance-regression-gate: ${state.invocationCount} invocation(s), ${state.comparisonCount} comparison(s)`,
|
|
302
|
+
counters: {
|
|
303
|
+
invocations: state.invocationCount,
|
|
304
|
+
comparisons: state.comparisonCount,
|
|
305
|
+
regressions: state.regressionCount,
|
|
306
|
+
missingResults: state.missingResultsCount,
|
|
307
|
+
errors: state.errorCount
|
|
308
|
+
},
|
|
309
|
+
lastResult: state.lastResult
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
var performance_regression_gate_default = plugin;
|
|
314
|
+
|
|
315
|
+
export { performance_regression_gate_default as default };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* plugin-stack-observer — observability over the providerRunner wrap stack.
|
|
5
|
+
*
|
|
6
|
+
* Several official plugins (llm-cache, model-router, prompt-firewall,
|
|
7
|
+
* token-throttle) register `AgentExtension.wrapProviderRunner` to
|
|
8
|
+
* intercept every provider call. The runtime composes them in
|
|
9
|
+
* registration order — but until now there was no way for an operator
|
|
10
|
+
* (or the LLM itself) to see WHICH wraps are active in what order.
|
|
11
|
+
*
|
|
12
|
+
* Each wrap plugin emits `customEvent('provider.wrap:loaded', …)` on
|
|
13
|
+
* setup. This plugin subscribes via `onPattern('provider.wrap:loaded')`
|
|
14
|
+
* and:
|
|
15
|
+
*
|
|
16
|
+
* 1. Builds an in-memory stack (preserves emission order)
|
|
17
|
+
* 2. Registers a system-prompt contributor that, when any wraps are
|
|
18
|
+
* present, emits a compact one-line block listing them. The LLM
|
|
19
|
+
* sees who is wrapping what, in registration order — making the
|
|
20
|
+
* stack visible without /diag round-trips.
|
|
21
|
+
*
|
|
22
|
+
* Disabled by default (enabled=false) because not every user wants
|
|
23
|
+
* the LLM to be aware of the wrap stack. The plugin_stack tool is
|
|
24
|
+
* always registered so operators can read the stack at runtime
|
|
25
|
+
* regardless of the contributor setting.
|
|
26
|
+
*
|
|
27
|
+
* Tools registered:
|
|
28
|
+
* - plugin_stack_status : Read the active wrap stack + load order
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
declare const PLUGIN: Plugin;
|
|
34
|
+
|
|
35
|
+
export { PLUGIN as default };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// src/plugin-stack-observer/index.ts
|
|
2
|
+
var state = {
|
|
3
|
+
wraps: [],
|
|
4
|
+
contributions: 0,
|
|
5
|
+
patternUnregister: null
|
|
6
|
+
};
|
|
7
|
+
function clearObserverState() {
|
|
8
|
+
if (state.patternUnregister) {
|
|
9
|
+
try {
|
|
10
|
+
state.patternUnregister();
|
|
11
|
+
} catch {
|
|
12
|
+
}
|
|
13
|
+
state.patternUnregister = null;
|
|
14
|
+
}
|
|
15
|
+
state.wraps = [];
|
|
16
|
+
state.contributions = 0;
|
|
17
|
+
}
|
|
18
|
+
var PLUGIN = {
|
|
19
|
+
name: "plugin-stack-observer",
|
|
20
|
+
version: "0.1.0",
|
|
21
|
+
description: "Observes the wrapProviderRunner stack and exposes it to operators (plugin_stack_status) and, optionally, to the LLM (system-prompt contributor).",
|
|
22
|
+
apiVersion: "^0.1.10",
|
|
23
|
+
capabilities: { tools: true },
|
|
24
|
+
defaultConfig: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
/** Inject the stack into the LLM's system prompt. */
|
|
27
|
+
injectIntoSystemPrompt: false
|
|
28
|
+
},
|
|
29
|
+
configSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
enabled: {
|
|
33
|
+
type: "boolean",
|
|
34
|
+
default: true,
|
|
35
|
+
description: "Master switch. When false, no events are collected."
|
|
36
|
+
},
|
|
37
|
+
injectIntoSystemPrompt: {
|
|
38
|
+
type: "boolean",
|
|
39
|
+
default: false,
|
|
40
|
+
description: "When true, the active wrap stack is included as a TextBlock in every system-prompt build so the LLM knows who is wrapping its provider calls."
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
setup(api) {
|
|
45
|
+
clearObserverState();
|
|
46
|
+
const cfg = readConfig(api.config.extensions?.["plugin-stack-observer"]);
|
|
47
|
+
if (!cfg.enabled) {
|
|
48
|
+
api.log.info("plugin-stack-observer loaded (disabled)");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
state.patternUnregister = api.onPattern(
|
|
52
|
+
"provider.wrap:loaded",
|
|
53
|
+
(_eventName, payload) => {
|
|
54
|
+
const p = payload ?? {};
|
|
55
|
+
if (typeof p.plugin !== "string" || p.plugin.length === 0) return;
|
|
56
|
+
const wraps = Array.isArray(p.wraps) ? p.wraps.filter((w) => typeof w === "string") : [];
|
|
57
|
+
const kind = typeof p.kind === "string" ? p.kind : "unknown";
|
|
58
|
+
state.wraps.push({
|
|
59
|
+
plugin: p.plugin,
|
|
60
|
+
kind,
|
|
61
|
+
wraps,
|
|
62
|
+
loadedAt: Date.now()
|
|
63
|
+
});
|
|
64
|
+
api.metrics.counter("wrap_loaded");
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
if (cfg.injectIntoSystemPrompt) {
|
|
68
|
+
api.registerSystemPromptContributor(async () => {
|
|
69
|
+
if (state.wraps.length === 0) return [];
|
|
70
|
+
state.contributions += 1;
|
|
71
|
+
api.metrics.counter("system_prompt_contribution");
|
|
72
|
+
const lines = state.wraps.map(
|
|
73
|
+
(w, i) => `${i + 1}. ${w.plugin} [${w.kind}] wraps: ${w.wraps.join(", ")}`
|
|
74
|
+
);
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `[wrapProviderRunner stack \u2014 registration order]
|
|
79
|
+
The following plugins wrap every LLM provider call in this order:
|
|
80
|
+
` + lines.join("\n") + `
|
|
81
|
+
Any failure or latency above is attributable to one of the above.`
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
api.tools.register({
|
|
87
|
+
name: "plugin_stack_status",
|
|
88
|
+
description: "Returns the active wrapProviderRunner stack: plugin names, kinds, what they wrap, and registration order.",
|
|
89
|
+
inputSchema: { type: "object", properties: {} },
|
|
90
|
+
permission: "auto",
|
|
91
|
+
category: "Diagnostics",
|
|
92
|
+
mutating: false,
|
|
93
|
+
async execute() {
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
enabled: cfg.enabled,
|
|
97
|
+
injectIntoSystemPrompt: cfg.injectIntoSystemPrompt,
|
|
98
|
+
wrapCount: state.wraps.length,
|
|
99
|
+
wraps: state.wraps,
|
|
100
|
+
contributions: state.contributions
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
api.log.info("plugin-stack-observer loaded", {
|
|
105
|
+
enabled: cfg.enabled,
|
|
106
|
+
injectIntoSystemPrompt: cfg.injectIntoSystemPrompt
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
teardown(api) {
|
|
110
|
+
const final = {
|
|
111
|
+
wrapCount: state.wraps.length,
|
|
112
|
+
contributions: state.contributions
|
|
113
|
+
};
|
|
114
|
+
clearObserverState();
|
|
115
|
+
api.log.info("plugin-stack-observer: teardown complete", { final });
|
|
116
|
+
},
|
|
117
|
+
async health() {
|
|
118
|
+
const stack = state.wraps.length === 0 ? "no wraps active" : state.wraps.map((w) => w.plugin).join(" \u2192 ");
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
message: `plugin-stack-observer: ${state.wraps.length} wrap(s) loaded [${stack}]`,
|
|
122
|
+
wrapCount: state.wraps.length,
|
|
123
|
+
contributions: state.contributions,
|
|
124
|
+
wraps: state.wraps
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
function readConfig(raw) {
|
|
129
|
+
if (!raw || typeof raw !== "object") return { enabled: true, injectIntoSystemPrompt: false };
|
|
130
|
+
const r = raw;
|
|
131
|
+
return {
|
|
132
|
+
enabled: r["enabled"] !== false,
|
|
133
|
+
injectIntoSystemPrompt: r["injectIntoSystemPrompt"] === true
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
var plugin_stack_observer_default = PLUGIN;
|
|
137
|
+
|
|
138
|
+
export { plugin_stack_observer_default as default };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pr-drafter plugin — collects work done during a session and drafts a
|
|
5
|
+
* pull-request description.
|
|
6
|
+
*
|
|
7
|
+
* As the session runs, the plugin listens to:
|
|
8
|
+
* - `tool.completed` events for `git_autocommit` (records commit messages)
|
|
9
|
+
* - `tool.completed` events for `write`/`edit` (records changed files)
|
|
10
|
+
* - `provider.response` events (records model usage)
|
|
11
|
+
*
|
|
12
|
+
* On `Stop` it composes a markdown PR body and writes it to a configurable
|
|
13
|
+
* file (default: `.wrongstack/PR_DRAFT.md`). A `pr_draft` tool lets the
|
|
14
|
+
* agent request the draft on demand.
|
|
15
|
+
*
|
|
16
|
+
* Config (`config.extensions['pr-drafter']`):
|
|
17
|
+
*
|
|
18
|
+
* ```jsonc
|
|
19
|
+
* {
|
|
20
|
+
* "enabled": true,
|
|
21
|
+
* "outputPath": ".wrongstack/PR_DRAFT.md",
|
|
22
|
+
* "writeOnStop": true,
|
|
23
|
+
* "includeDiff": true,
|
|
24
|
+
* "includeCommits": true,
|
|
25
|
+
* "includeFiles": true,
|
|
26
|
+
* "aiSummary": false
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
declare const plugin: Plugin;
|
|
34
|
+
|
|
35
|
+
export { plugin as default };
|