@wrongstack/plugins 0.277.2 → 0.280.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 +838 -0
- package/dist/auto-doc.d.ts +8 -0
- package/dist/auto-doc.js +175 -13
- package/dist/auto-escalate.d.ts +45 -0
- package/dist/auto-escalate.js +190 -0
- package/dist/branch-guard.d.ts +33 -0
- package/dist/branch-guard.js +228 -0
- package/dist/changelog-writer.d.ts +73 -0
- package/dist/changelog-writer.js +369 -0
- package/dist/checkpoint.d.ts +55 -0
- package/dist/checkpoint.js +305 -0
- package/dist/commit-validator.d.ts +33 -0
- package/dist/commit-validator.js +315 -0
- package/dist/config-validator.d.ts +48 -0
- package/dist/config-validator.js +347 -0
- package/dist/context-pins.d.ts +45 -0
- package/dist/context-pins.js +240 -0
- package/dist/cost-tracker.d.ts +40 -1
- package/dist/cost-tracker.js +105 -4
- package/dist/dep-guard.d.ts +65 -0
- package/dist/dep-guard.js +316 -0
- package/dist/diff-summary.d.ts +36 -0
- package/dist/diff-summary.js +235 -0
- package/dist/error-lens.d.ts +67 -0
- package/dist/error-lens.js +280 -0
- package/dist/format-on-save.d.ts +35 -0
- package/dist/format-on-save.js +219 -0
- package/dist/git-autocommit.js +186 -26
- package/dist/import-organizer.d.ts +52 -0
- package/dist/import-organizer.js +274 -0
- package/dist/index.d.ts +32 -6
- package/dist/index.js +10151 -1628
- package/dist/injection-shield.d.ts +49 -0
- package/dist/injection-shield.js +205 -0
- package/dist/lint-gate.d.ts +33 -0
- package/dist/lint-gate.js +394 -0
- package/dist/llm-cache.d.ts +56 -0
- package/dist/llm-cache.js +251 -0
- package/dist/loop-breaker.d.ts +43 -0
- package/dist/loop-breaker.js +241 -0
- package/dist/model-router.d.ts +69 -0
- package/dist/model-router.js +198 -0
- package/dist/notify-hub.d.ts +45 -0
- package/dist/notify-hub.js +304 -0
- package/dist/path-guard.d.ts +54 -0
- package/dist/path-guard.js +235 -0
- package/dist/prompt-firewall.d.ts +57 -0
- package/dist/prompt-firewall.js +290 -0
- package/dist/secret-scanner.d.ts +34 -0
- package/dist/secret-scanner.js +409 -0
- package/dist/semver-bump.js +45 -0
- package/dist/session-recap.d.ts +50 -0
- package/dist/session-recap.js +421 -0
- package/dist/shell-check.js +52 -4
- package/dist/spec-linker.d.ts +51 -0
- package/dist/spec-linker.js +541 -0
- package/dist/template-engine.js +19 -1
- package/dist/test-runner-gate.d.ts +37 -0
- package/dist/test-runner-gate.js +356 -0
- package/dist/todo-listener.d.ts +37 -0
- package/dist/todo-listener.js +216 -0
- package/dist/todo-tracker.d.ts +5 -0
- package/dist/todo-tracker.js +441 -0
- package/dist/token-budget.d.ts +40 -0
- package/dist/token-budget.js +254 -0
- package/dist/token-throttle.d.ts +54 -0
- package/dist/token-throttle.js +203 -0
- package/package.json +116 -12
- package/dist/json-path.d.ts +0 -18
- package/dist/json-path.js +0 -15
- package/dist/web-search.d.ts +0 -19
- package/dist/web-search.js +0 -15
package/dist/auto-doc.d.ts
CHANGED
|
@@ -7,6 +7,14 @@ import { Plugin } from '@wrongstack/core';
|
|
|
7
7
|
* - auto_doc: Generate and inject doc comments into JS/TS files.
|
|
8
8
|
* Pass `dry_run: true` to preview without writing (replaces the former
|
|
9
9
|
* `auto_doc (dry_run)` tool).
|
|
10
|
+
*
|
|
11
|
+
* LLM mode (opt-in): with `config.extensions['auto-doc'].useLlm = true`
|
|
12
|
+
* (or per-call `use_llm: true`) and a host that wires `api.llm`, each
|
|
13
|
+
* doc comment's prose is written by the LLM from the entity's signature
|
|
14
|
+
* and body instead of the `TODO: describe …` placeholder. Provider/model
|
|
15
|
+
* follow the plugin's `config.extensions['auto-doc'].llm` override, then
|
|
16
|
+
* the session default. Best-effort: any LLM failure (or a bad response)
|
|
17
|
+
* falls back to the template for that entity — a doc comment always lands.
|
|
10
18
|
*/
|
|
11
19
|
|
|
12
20
|
declare const plugin: Plugin;
|
package/dist/auto-doc.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
// src/auto-doc/index.ts
|
|
2
2
|
var AUTO_DOC_API_VERSION = "^0.1.10";
|
|
3
|
+
var state = {
|
|
4
|
+
invocationCount: 0,
|
|
5
|
+
/** Doc comments whose prose came from the LLM this session. */
|
|
6
|
+
llmDocs: 0,
|
|
7
|
+
/** LLM calls that failed and fell back to the template. */
|
|
8
|
+
llmFallbacks: 0,
|
|
9
|
+
/** Last invocation summary — surfaced by health() for /diag plugins. */
|
|
10
|
+
lastInvocation: null
|
|
11
|
+
};
|
|
3
12
|
function parseSource(content) {
|
|
4
13
|
const entities = [];
|
|
5
14
|
const lines = content.split("\n");
|
|
@@ -75,6 +84,54 @@ ${params}${returns}
|
|
|
75
84
|
*/`;
|
|
76
85
|
}
|
|
77
86
|
}
|
|
87
|
+
function entitySnippet(content, entity, maxLines = 25) {
|
|
88
|
+
const lines = content.split("\n");
|
|
89
|
+
const start = entity.startLine - 1;
|
|
90
|
+
return lines.slice(start, start + maxLines).join("\n");
|
|
91
|
+
}
|
|
92
|
+
async function generateDocCommentLlm(entity, snippet, includeTypes, api) {
|
|
93
|
+
if (!api.llm) return null;
|
|
94
|
+
const paramList = entity.kind === "function" ? entity.params : [];
|
|
95
|
+
try {
|
|
96
|
+
const result = await api.llm.complete(
|
|
97
|
+
`Write documentation for this ${entity.kind} named "${entity.name}". Respond with ONLY a JSON object of the form {"summary": string, "params": {"<name>": string}, "returns": string}. summary is one concise sentence. params has one entry per parameter` + (paramList.length > 0 ? ` (${paramList.join(", ")})` : " (may be empty)") + ". returns describes the return value (empty string if none). No prose outside the JSON.\n\n```\n" + snippet + "\n```",
|
|
98
|
+
{
|
|
99
|
+
system: "You are a precise API documentation writer. Output only JSON.",
|
|
100
|
+
maxTokens: 400,
|
|
101
|
+
responseFormat: "json"
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
const parsed = JSON.parse(extractJsonObject(result.text));
|
|
105
|
+
const summary = typeof parsed.summary === "string" && parsed.summary.trim() ? parsed.summary.trim() : `Describe ${entity.name}`;
|
|
106
|
+
if (entity.kind !== "function") {
|
|
107
|
+
return `/**
|
|
108
|
+
* ${summary}
|
|
109
|
+
*/`;
|
|
110
|
+
}
|
|
111
|
+
const paramLines = paramList.map((p) => {
|
|
112
|
+
const desc = parsed.params && typeof parsed.params[p] === "string" && parsed.params[p].trim() ? parsed.params[p].trim() : "parameter";
|
|
113
|
+
return ` * @param ${p} - ${desc}`;
|
|
114
|
+
});
|
|
115
|
+
const returnsDesc = typeof parsed.returns === "string" && parsed.returns.trim() ? parsed.returns.trim() : "";
|
|
116
|
+
const returnsLine = entity.returnType && returnsDesc ? `
|
|
117
|
+
* @returns ${includeTypes ? `{${entity.returnType}} ` : ""}${returnsDesc}` : entity.returnType ? `
|
|
118
|
+
* @returns ${includeTypes ? `{${entity.returnType}} ` : ""}return value` : "";
|
|
119
|
+
const paramBlock = paramLines.length > 0 ? `
|
|
120
|
+
${paramLines.join("\n")}` : "";
|
|
121
|
+
return `/**
|
|
122
|
+
* ${summary}${paramBlock}${returnsLine}
|
|
123
|
+
*/`;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function extractJsonObject(text) {
|
|
129
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(text);
|
|
130
|
+
const body = fenced?.[1] ?? text;
|
|
131
|
+
const start = body.indexOf("{");
|
|
132
|
+
const end = body.lastIndexOf("}");
|
|
133
|
+
return start >= 0 && end > start ? body.slice(start, end + 1) : body.trim();
|
|
134
|
+
}
|
|
78
135
|
function needsDocComment(content, entity) {
|
|
79
136
|
const lines = content.split("\n");
|
|
80
137
|
const lineIdx = entity.startLine - 1;
|
|
@@ -92,13 +149,27 @@ function injectDocComment(content, entity, doc) {
|
|
|
92
149
|
}
|
|
93
150
|
async function runAutoDoc(input, api) {
|
|
94
151
|
if (!input.files || typeof input.files !== "object" || !Array.isArray(input.files)) {
|
|
95
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
error: "input.files must be an array of file paths",
|
|
155
|
+
filesProcessed: 0,
|
|
156
|
+
changes: []
|
|
157
|
+
};
|
|
96
158
|
}
|
|
97
159
|
if (input.files.length === 0) {
|
|
98
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
error: "input.files is empty \u2014 provide at least one file path",
|
|
163
|
+
filesProcessed: 0,
|
|
164
|
+
changes: []
|
|
165
|
+
};
|
|
99
166
|
}
|
|
100
|
-
const
|
|
167
|
+
const extConfig = api.config.extensions?.["auto-doc"] ?? {};
|
|
168
|
+
const includeTypes = extConfig["includeTypes"] ?? false;
|
|
169
|
+
const useLlm = (input.use_llm ?? extConfig["useLlm"] ?? false) === true && Boolean(api.llm);
|
|
170
|
+
const maxLlmEntities = typeof extConfig["maxLlmEntities"] === "number" && extConfig["maxLlmEntities"] >= 0 ? extConfig["maxLlmEntities"] : 25;
|
|
101
171
|
const results = [];
|
|
172
|
+
let llmBudget = maxLlmEntities;
|
|
102
173
|
for (const file of input.files) {
|
|
103
174
|
try {
|
|
104
175
|
const { readFileSync, writeFileSync } = await import('fs');
|
|
@@ -113,9 +184,28 @@ async function runAutoDoc(input, api) {
|
|
|
113
184
|
let modified = content;
|
|
114
185
|
for (const entity of entities) {
|
|
115
186
|
if (!input.force && !needsDocComment(modified, entity)) continue;
|
|
116
|
-
|
|
187
|
+
let doc = null;
|
|
188
|
+
let source = "template";
|
|
189
|
+
if (useLlm && llmBudget > 0) {
|
|
190
|
+
llmBudget -= 1;
|
|
191
|
+
doc = await generateDocCommentLlm(
|
|
192
|
+
entity,
|
|
193
|
+
entitySnippet(modified, entity),
|
|
194
|
+
includeTypes,
|
|
195
|
+
api
|
|
196
|
+
);
|
|
197
|
+
if (doc) {
|
|
198
|
+
source = "llm";
|
|
199
|
+
state.llmDocs += 1;
|
|
200
|
+
api.metrics.counter("llm_docs");
|
|
201
|
+
} else {
|
|
202
|
+
state.llmFallbacks += 1;
|
|
203
|
+
api.metrics.counter("llm_fallbacks");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!doc) doc = generateDocComment(entity, includeTypes);
|
|
117
207
|
modified = injectDocComment(modified, entity, doc);
|
|
118
|
-
results.push({ file, entity: entity.name });
|
|
208
|
+
results.push({ file, entity: entity.name, source });
|
|
119
209
|
}
|
|
120
210
|
if (!input.dry_run && results.length > 0) {
|
|
121
211
|
writeFileSync(file, modified, "utf-8");
|
|
@@ -125,7 +215,12 @@ async function runAutoDoc(input, api) {
|
|
|
125
215
|
api.log.error(`auto-doc: error processing ${file}: ${err}`);
|
|
126
216
|
}
|
|
127
217
|
}
|
|
128
|
-
return {
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
filesProcessed: input.files.length,
|
|
221
|
+
changes: results,
|
|
222
|
+
llm: useLlm ? { docs: state.llmDocs, fallbacks: state.llmFallbacks } : void 0
|
|
223
|
+
};
|
|
129
224
|
}
|
|
130
225
|
var plugin = {
|
|
131
226
|
name: "auto-doc",
|
|
@@ -133,26 +228,68 @@ var plugin = {
|
|
|
133
228
|
description: "Auto-generates JSDoc/TSDoc comments for functions, classes, types, and interfaces",
|
|
134
229
|
apiVersion: AUTO_DOC_API_VERSION,
|
|
135
230
|
capabilities: { tools: true, pipelines: ["toolCall"] },
|
|
136
|
-
defaultConfig: {
|
|
231
|
+
defaultConfig: {
|
|
232
|
+
style: "tsdoc",
|
|
233
|
+
includeTypes: false,
|
|
234
|
+
dryRun: false,
|
|
235
|
+
useLlm: false,
|
|
236
|
+
maxLlmEntities: 25
|
|
237
|
+
},
|
|
137
238
|
configSchema: {
|
|
138
239
|
type: "object",
|
|
139
240
|
properties: {
|
|
140
241
|
style: { type: "string", enum: ["jsdoc", "tsdoc"], default: "tsdoc" },
|
|
141
242
|
includeTypes: { type: "boolean", default: false },
|
|
142
|
-
dryRun: { type: "boolean", default: false }
|
|
243
|
+
dryRun: { type: "boolean", default: false },
|
|
244
|
+
useLlm: {
|
|
245
|
+
type: "boolean",
|
|
246
|
+
default: false,
|
|
247
|
+
description: 'Write doc prose with the LLM (api.llm) instead of TODO placeholders. Provider/model follow extensions["auto-doc"].llm, then the session default.'
|
|
248
|
+
},
|
|
249
|
+
maxLlmEntities: {
|
|
250
|
+
type: "number",
|
|
251
|
+
minimum: 0,
|
|
252
|
+
default: 25,
|
|
253
|
+
description: "Cap on LLM-written doc comments per invocation; entities beyond it use the template."
|
|
254
|
+
},
|
|
255
|
+
llm: {
|
|
256
|
+
type: "object",
|
|
257
|
+
description: "Optional { provider, model } override for LLM doc prose."
|
|
258
|
+
}
|
|
143
259
|
}
|
|
144
260
|
},
|
|
145
261
|
setup(api) {
|
|
262
|
+
state.invocationCount = 0;
|
|
263
|
+
state.llmDocs = 0;
|
|
264
|
+
state.llmFallbacks = 0;
|
|
265
|
+
state.lastInvocation = null;
|
|
146
266
|
api.tools.register({
|
|
147
267
|
name: "auto_doc",
|
|
148
268
|
description: "Auto-generate JSDoc/TSDoc comments for functions, classes, types, and interfaces in source files. Set `dry_run: true` to preview without writing.",
|
|
149
269
|
inputSchema: {
|
|
150
270
|
type: "object",
|
|
151
271
|
properties: {
|
|
152
|
-
files: {
|
|
153
|
-
|
|
272
|
+
files: {
|
|
273
|
+
type: "array",
|
|
274
|
+
items: { type: "string" },
|
|
275
|
+
description: "Source files to document"
|
|
276
|
+
},
|
|
277
|
+
style: {
|
|
278
|
+
type: "string",
|
|
279
|
+
enum: ["jsdoc", "tsdoc"],
|
|
280
|
+
default: "tsdoc",
|
|
281
|
+
description: "Comment style"
|
|
282
|
+
},
|
|
154
283
|
force: { type: "boolean", default: false, description: "Overwrite existing docstrings" },
|
|
155
|
-
dry_run: {
|
|
284
|
+
dry_run: {
|
|
285
|
+
type: "boolean",
|
|
286
|
+
default: false,
|
|
287
|
+
description: "Preview generated comments without writing to files"
|
|
288
|
+
},
|
|
289
|
+
use_llm: {
|
|
290
|
+
type: "boolean",
|
|
291
|
+
description: "Write doc prose with the LLM (api.llm) instead of TODO placeholders. Overrides the useLlm config flag for this call."
|
|
292
|
+
}
|
|
156
293
|
},
|
|
157
294
|
required: ["files"]
|
|
158
295
|
},
|
|
@@ -160,13 +297,38 @@ var plugin = {
|
|
|
160
297
|
mutating: true,
|
|
161
298
|
category: "Project",
|
|
162
299
|
async execute(input) {
|
|
163
|
-
|
|
300
|
+
const inp = input;
|
|
301
|
+
state.invocationCount += 1;
|
|
302
|
+
const result = await runAutoDoc(inp, api);
|
|
303
|
+
state.lastInvocation = {
|
|
304
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
305
|
+
files: Array.isArray(inp.files) ? inp.files.length : 0,
|
|
306
|
+
style: inp.style === "jsdoc" ? "jsdoc" : "tsdoc",
|
|
307
|
+
dryRun: inp.dry_run === true
|
|
308
|
+
};
|
|
309
|
+
return result;
|
|
164
310
|
}
|
|
165
311
|
});
|
|
166
312
|
api.log.info("auto-doc plugin loaded", { version: "0.2.0", capabilities: ["auto_doc"] });
|
|
167
313
|
},
|
|
168
314
|
teardown(api) {
|
|
169
|
-
|
|
315
|
+
const finalCount = state.invocationCount;
|
|
316
|
+
const finalLlmDocs = state.llmDocs;
|
|
317
|
+
state.invocationCount = 0;
|
|
318
|
+
state.llmDocs = 0;
|
|
319
|
+
state.llmFallbacks = 0;
|
|
320
|
+
state.lastInvocation = null;
|
|
321
|
+
api.log.info("auto-doc: teardown complete", { invocations: finalCount, llmDocs: finalLlmDocs });
|
|
322
|
+
},
|
|
323
|
+
async health() {
|
|
324
|
+
return {
|
|
325
|
+
ok: true,
|
|
326
|
+
message: state.lastInvocation === null ? `auto-doc: ${state.invocationCount} invocation(s) this session` : `auto-doc: last run ${state.lastInvocation.files} file(s) at ${state.lastInvocation.when} (${state.lastInvocation.dryRun ? "dry-run" : "write"}); ${state.llmDocs} LLM doc(s), ${state.llmFallbacks} fallback(s)`,
|
|
327
|
+
invocationCount: state.invocationCount,
|
|
328
|
+
llmDocs: state.llmDocs,
|
|
329
|
+
llmFallbacks: state.llmFallbacks,
|
|
330
|
+
lastInvocation: state.lastInvocation
|
|
331
|
+
};
|
|
170
332
|
}
|
|
171
333
|
};
|
|
172
334
|
var auto_doc_default = plugin;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* auto-escalate plugin — turns transient provider errors into a graceful
|
|
5
|
+
* model-escalation ladder instead of a hard failure.
|
|
6
|
+
*
|
|
7
|
+
* Registers an `AgentExtension.onError` hook (wired at agent-loop.ts:636).
|
|
8
|
+
* When a provider call fails with a retryable error (overload, rate
|
|
9
|
+
* limit, timeout, 5xx), the plugin asks the agent loop to retry the turn
|
|
10
|
+
* with the NEXT model in a configured escalation ladder — e.g. a busy
|
|
11
|
+
* fast model escalates to a more capable one that may have spare
|
|
12
|
+
* capacity. On a non-retryable error, or once the ladder is exhausted,
|
|
13
|
+
* it steps aside (returns nothing) so the host's built-in error recovery
|
|
14
|
+
* runs unchanged.
|
|
15
|
+
*
|
|
16
|
+
* Loop constraint: the agent loop caps recovery retries at 2 per turn
|
|
17
|
+
* (agent-loop.ts), so at most the first ~2 rungs of the ladder are used
|
|
18
|
+
* before the loop fails. Order the ladder cheapest→most-capable.
|
|
19
|
+
*
|
|
20
|
+
* Safety posture: opt-in — loads inert until
|
|
21
|
+
* `config.extensions['auto-escalate'].enabled = true`. It never forces a
|
|
22
|
+
* `fail`; it only requests retries with a better model, and otherwise
|
|
23
|
+
* defers to the default handler.
|
|
24
|
+
*
|
|
25
|
+
* Config (`config.extensions['auto-escalate']`):
|
|
26
|
+
*
|
|
27
|
+
* ```jsonc
|
|
28
|
+
* {
|
|
29
|
+
* "enabled": false,
|
|
30
|
+
* "escalation": ["claude-sonnet-5", "claude-opus-4-8"],
|
|
31
|
+
* "retryablePatterns": ["overload", "rate.?limit", "429", "50[023]", "timeout", "ETIMEDOUT", "ECONNRESET"]
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* Tools:
|
|
36
|
+
* - `auto_escalate_status` — ladder, patterns, and escalation counters
|
|
37
|
+
*
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
declare function errorText(err: unknown): string;
|
|
42
|
+
declare function isRetryable(text: string, patterns: RegExp[]): boolean;
|
|
43
|
+
declare const plugin: Plugin;
|
|
44
|
+
|
|
45
|
+
export { plugin as default, errorText, isRetryable };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// src/auto-escalate/index.ts
|
|
2
|
+
var DEFAULT_PATTERNS = [
|
|
3
|
+
"overload",
|
|
4
|
+
"rate.?limit",
|
|
5
|
+
"\\b429\\b",
|
|
6
|
+
"\\b50[023]\\b",
|
|
7
|
+
"timeout",
|
|
8
|
+
"ETIMEDOUT",
|
|
9
|
+
"ECONNRESET",
|
|
10
|
+
"ECONNREFUSED",
|
|
11
|
+
"temporarily unavailable"
|
|
12
|
+
];
|
|
13
|
+
function readConfig(raw) {
|
|
14
|
+
const base = {
|
|
15
|
+
enabled: false,
|
|
16
|
+
escalation: [],
|
|
17
|
+
retryablePatterns: DEFAULT_PATTERNS.map((p) => new RegExp(p, "i"))
|
|
18
|
+
};
|
|
19
|
+
if (!raw || typeof raw !== "object") return base;
|
|
20
|
+
const r = raw;
|
|
21
|
+
const escalation = Array.isArray(r["escalation"]) ? r["escalation"].filter((m) => typeof m === "string" && m.length > 0) : [];
|
|
22
|
+
const patterns = Array.isArray(r["retryablePatterns"]) ? r["retryablePatterns"].filter((p) => typeof p === "string" && p.length > 0).flatMap((p) => {
|
|
23
|
+
try {
|
|
24
|
+
return [new RegExp(p, "i")];
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}) : base.retryablePatterns;
|
|
29
|
+
return {
|
|
30
|
+
enabled: r["enabled"] === true,
|
|
31
|
+
escalation,
|
|
32
|
+
retryablePatterns: patterns.length > 0 ? patterns : base.retryablePatterns
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
var state = {
|
|
36
|
+
errorsSeen: 0,
|
|
37
|
+
escalationsRequested: 0,
|
|
38
|
+
laddersExhausted: 0,
|
|
39
|
+
runEscalations: 0,
|
|
40
|
+
lastEscalation: null,
|
|
41
|
+
extensionUnregister: null
|
|
42
|
+
};
|
|
43
|
+
function errorText(err) {
|
|
44
|
+
if (err instanceof Error) return `${err.name}: ${err.message}`;
|
|
45
|
+
if (typeof err === "string") return err;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.stringify(err);
|
|
48
|
+
} catch {
|
|
49
|
+
return String(err);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function isRetryable(text, patterns) {
|
|
53
|
+
return patterns.some((re) => re.test(text));
|
|
54
|
+
}
|
|
55
|
+
var plugin = {
|
|
56
|
+
name: "auto-escalate",
|
|
57
|
+
version: "0.1.0",
|
|
58
|
+
description: "On retryable provider errors, retries the turn with the next model in an escalation ladder (onError). Opt-in; defers to default recovery otherwise.",
|
|
59
|
+
apiVersion: "^0.1.10",
|
|
60
|
+
capabilities: { tools: true },
|
|
61
|
+
defaultConfig: { enabled: false, escalation: [], retryablePatterns: DEFAULT_PATTERNS },
|
|
62
|
+
configSchema: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
enabled: {
|
|
66
|
+
type: "boolean",
|
|
67
|
+
default: false,
|
|
68
|
+
description: "Master switch. OFF by default because it changes error-recovery behavior."
|
|
69
|
+
},
|
|
70
|
+
escalation: {
|
|
71
|
+
type: "array",
|
|
72
|
+
items: { type: "string" },
|
|
73
|
+
default: [],
|
|
74
|
+
description: "Ordered model ids (cheapest\u2192most-capable) to retry with on successive retryable errors."
|
|
75
|
+
},
|
|
76
|
+
retryablePatterns: {
|
|
77
|
+
type: "array",
|
|
78
|
+
items: { type: "string" },
|
|
79
|
+
description: "Case-insensitive regexes; an error whose text matches any is treated as retryable."
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
setup(api) {
|
|
84
|
+
state.errorsSeen = 0;
|
|
85
|
+
state.escalationsRequested = 0;
|
|
86
|
+
state.laddersExhausted = 0;
|
|
87
|
+
state.runEscalations = 0;
|
|
88
|
+
state.lastEscalation = null;
|
|
89
|
+
if (state.extensionUnregister) {
|
|
90
|
+
try {
|
|
91
|
+
state.extensionUnregister();
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
state.extensionUnregister = null;
|
|
95
|
+
}
|
|
96
|
+
const cfg = readConfig(api.config.extensions?.["auto-escalate"]);
|
|
97
|
+
if (cfg.enabled && cfg.escalation.length > 0) {
|
|
98
|
+
state.extensionUnregister = api.extensions.register({
|
|
99
|
+
name: "auto-escalate",
|
|
100
|
+
owner: "auto-escalate",
|
|
101
|
+
// Fresh run → reset the ladder position.
|
|
102
|
+
beforeRun() {
|
|
103
|
+
state.runEscalations = 0;
|
|
104
|
+
},
|
|
105
|
+
onError(_ctx, err, phase) {
|
|
106
|
+
if (phase !== "provider") return;
|
|
107
|
+
state.errorsSeen += 1;
|
|
108
|
+
const text = errorText(err);
|
|
109
|
+
if (!isRetryable(text, cfg.retryablePatterns)) return;
|
|
110
|
+
if (state.runEscalations >= cfg.escalation.length) {
|
|
111
|
+
state.laddersExhausted += 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const nextModel = cfg.escalation[state.runEscalations];
|
|
115
|
+
state.runEscalations += 1;
|
|
116
|
+
state.escalationsRequested += 1;
|
|
117
|
+
state.lastEscalation = {
|
|
118
|
+
to: nextModel,
|
|
119
|
+
reason: text.slice(0, 200),
|
|
120
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
121
|
+
};
|
|
122
|
+
api.metrics.counter("escalations", 1, { to: nextModel });
|
|
123
|
+
api.log.warn("auto-escalate: retrying with escalated model", { to: nextModel });
|
|
124
|
+
return { action: "retry", model: nextModel };
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
api.tools.register({
|
|
129
|
+
name: "auto_escalate_status",
|
|
130
|
+
description: "Reports auto-escalate state: enabled, escalation ladder, and escalation counters.",
|
|
131
|
+
inputSchema: { type: "object", properties: {} },
|
|
132
|
+
permission: "auto",
|
|
133
|
+
category: "Diagnostics",
|
|
134
|
+
mutating: false,
|
|
135
|
+
async execute() {
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
enabled: cfg.enabled,
|
|
139
|
+
escalation: cfg.escalation,
|
|
140
|
+
retryablePatterns: cfg.retryablePatterns.map((r) => r.source),
|
|
141
|
+
counters: {
|
|
142
|
+
errorsSeen: state.errorsSeen,
|
|
143
|
+
escalationsRequested: state.escalationsRequested,
|
|
144
|
+
laddersExhausted: state.laddersExhausted
|
|
145
|
+
},
|
|
146
|
+
lastEscalation: state.lastEscalation
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
api.log.info("auto-escalate plugin loaded", {
|
|
151
|
+
version: "0.1.0",
|
|
152
|
+
enabled: cfg.enabled,
|
|
153
|
+
ladder: cfg.escalation
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
teardown(api) {
|
|
157
|
+
if (state.extensionUnregister) {
|
|
158
|
+
try {
|
|
159
|
+
state.extensionUnregister();
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
state.extensionUnregister = null;
|
|
163
|
+
}
|
|
164
|
+
const final = {
|
|
165
|
+
errorsSeen: state.errorsSeen,
|
|
166
|
+
escalationsRequested: state.escalationsRequested,
|
|
167
|
+
laddersExhausted: state.laddersExhausted
|
|
168
|
+
};
|
|
169
|
+
state.errorsSeen = 0;
|
|
170
|
+
state.escalationsRequested = 0;
|
|
171
|
+
state.laddersExhausted = 0;
|
|
172
|
+
state.runEscalations = 0;
|
|
173
|
+
state.lastEscalation = null;
|
|
174
|
+
api.log.info("auto-escalate: teardown complete", { final });
|
|
175
|
+
},
|
|
176
|
+
async health() {
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
message: `auto-escalate: ${state.escalationsRequested} escalation(s) of ${state.errorsSeen} error(s), ${state.laddersExhausted} ladder(s) exhausted`,
|
|
180
|
+
counters: {
|
|
181
|
+
errorsSeen: state.errorsSeen,
|
|
182
|
+
escalationsRequested: state.escalationsRequested,
|
|
183
|
+
laddersExhausted: state.laddersExhausted
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
var auto_escalate_default = plugin;
|
|
189
|
+
|
|
190
|
+
export { auto_escalate_default as default, errorText, isRetryable };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* branch-guard plugin — PreToolUse hook that blocks commits, pushes,
|
|
5
|
+
* and merges to protected branches (default: main, master).
|
|
6
|
+
*
|
|
7
|
+
* Tools registered:
|
|
8
|
+
* - branch_guard_status : Show protected branches, mode, and counters.
|
|
9
|
+
*
|
|
10
|
+
* Hooks registered:
|
|
11
|
+
* - PreToolUse with matcher `bash|git_autocommit`. Inspects the tool
|
|
12
|
+
* input for git commit / push / merge / rebase commands (bash) or
|
|
13
|
+
* the tool call itself (git_autocommit). If the current branch is
|
|
14
|
+
* protected, the call is blocked with a clear reason.
|
|
15
|
+
*
|
|
16
|
+
* Config (`config.extensions['branch-guard']`):
|
|
17
|
+
*
|
|
18
|
+
* ```jsonc
|
|
19
|
+
* {
|
|
20
|
+
* "branches": ["main", "master"], // protected branch names
|
|
21
|
+
* "mode": "block", // "block" | "warn"
|
|
22
|
+
* "blockMerge": true, // also block merges into protected
|
|
23
|
+
* "blockPush": true, // also block pushes from protected
|
|
24
|
+
* "blockCommit": true // also block commits on protected
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
declare const plugin: Plugin;
|
|
32
|
+
|
|
33
|
+
export { plugin as default };
|