tewip-ai 0.55.0 → 0.57.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 CHANGED
@@ -15,13 +15,42 @@ It works for every stack: Node.js (node:test, Jest, Vitest, Mocha), Python (pyte
15
15
  | **Terminal** | `npm install -g tewip-ai`, then `tewip explain report.xml`, `tewip ci owner/repo --pr 12`, `tewip mcp` |
16
16
  | **VS Code, Cursor, Windsurf, VSCodium** | the Tewip extension ([download](https://github.com/hmamut39/tewip/releases/tag/vscode-0.5.0)): registers Tewip with the editor's AI assistant and adds six commands - explain a report, why CI failed on this branch, the risk review of your change, which flaky tests to quarantine, the security check, and what your corrections add up to |
17
17
  | **JetBrains, Claude, other AI assistants** | add the MCP server `tewip mcp`: fifteen tools, including *has this happened here before?*, *is this requirement covered and passing?* and *what should be done next about this goal?* (see [editor setup](#use-it-from-your-editor-any-ide-that-speaks-mcp)) |
18
- | **Any model** | OpenAI, Claude, Gemini, Azure, or a model on your own machine with Ollama (no account, nothing leaves the computer): `TEWIP_PROVIDER` |
18
+ | **Any model, including none** | OpenAI, Claude, Gemini, Azure, or a model on your own machine with Ollama - no account, no key, and no source code leaving the building: `TEWIP_PROVIDER=ollama`. Three quarters of what Tewip answers calls no model at all. [How](#running-with-no-data-leaving-the-building) |
19
19
  | **GitHub CI** | the Tewip Action: a comment on every pull request, an optional merge gate, verified fix PRs ([QUICKSTART](QUICKSTART.md)) |
20
20
 
21
21
  Formerly *triage-agent*: the repository moved to `hmamut39/tewip`, and GitHub redirects the old address. See [ROADMAP.md](ROADMAP.md) for the plan.
22
22
 
23
23
  **Status: phases 1 to 14 are built; the two auto-fix phases (5 and 8) are the ones still in progress.** Tewip collects failures from any framework, diagnoses them with rules first and a model only for what rules cannot settle, and turns that into work for each role: a CI comment and merge gate, test plans and verified tests, a flake dossier, a PR-risk review, security scanning, tickets in Jira, Rally, GitHub Issues, Azure Boards or Linear, checks generated from a Figma design, specifications written for people, and - opt in, and only when proved - fixes to test or application code. It runs live in GitHub CI and answers questions from AI assistants through MCP. On fresh real Gutenberg CI failures, with the pipeline frozen before labelling, accuracy is 93.4%; across six real datasets it ranges from 86.1% to 99.7%, and the caveats are in [eval/README.md](eval/README.md).
24
24
 
25
+
26
+ ## Running with no data leaving the building
27
+
28
+ A hospital, a bank or a government department usually cannot send source code to a hosted API, and
29
+ that is most of who this is built for. Tewip runs against a model on the same machine:
30
+
31
+ ```bash
32
+ # once
33
+ curl -fsSL https://ollama.com/install.sh | sh # or ollama.com for Windows and macOS
34
+ ollama pull qwen3:8b
35
+
36
+ # then
37
+ export TEWIP_PROVIDER=ollama
38
+ tewip done --diff main...HEAD
39
+ ```
40
+
41
+ No key, no account, no network call off the machine. `TEWIP_BASE_URL` points the same path at
42
+ LM Studio, vLLM, llama.cpp's server, or anything else that speaks the OpenAI shape.
43
+
44
+ **What changes and what does not.** The deterministic layers - `vouch`, `schema`, `access`, `perf`,
45
+ `standards` - call no model at any time, with any provider, and are unaffected. Only the parts that
46
+ need judgement use one: explaining a failure, writing a test, drafting a plan. A smaller local model
47
+ is less accurate at those than a frontier one, and Tewip says which layer decided every answer, so
48
+ the difference is visible rather than hidden.
49
+
50
+ A local model is also less likely to be *constrained* to the schema it was asked for: some backends
51
+ accept the request and quietly do not enforce it. Tewip reads a reply that arrives inside a markdown
52
+ fence or after a sentence of preamble, and still refuses one with no JSON in it at all.
53
+
25
54
  ## What it costs
26
55
 
27
56
  Most failures never reach a model. On 3,556 real CI failures from six public projects, the
@@ -50,7 +50,7 @@ export function loadDotEnv() {
50
50
  return;
51
51
  }
52
52
  }
53
- export function openAiClient({ apiKey, model, reasoningEffort, baseUrl = 'https://api.openai.com/v1' }) {
53
+ export function openAiClient({ apiKey, model, reasoningEffort, baseUrl = 'https://api.openai.com/v1', provider = 'OpenAI', hint = '' }) {
54
54
  return {
55
55
  model: reasoningEffort ? `${model} (reasoning ${reasoningEffort})` : model,
56
56
  async complete({ system, user, schemaName, schema }) {
@@ -75,8 +75,16 @@ export function openAiClient({ apiKey, model, reasoningEffort, baseUrl = 'https:
75
75
  });
76
76
  }
77
77
  catch (error) {
78
- if (attempt >= MAX_ATTEMPTS)
79
- throw new Error(`OpenAI request failed: ${String(error)}`);
78
+ if (attempt >= MAX_ATTEMPTS) {
79
+ // "OpenAI request failed: TypeError: fetch failed" is what somebody running a model on
80
+ // their own machine used to see when they had not started it - the wrong provider named,
81
+ // and no mention of the one thing wrong. A local endpoint that refuses the connection is
82
+ // not an outage, it is a server nobody started yet, and the hint says how.
83
+ const local = /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])/.test(baseUrl);
84
+ throw new Error(local
85
+ ? `no model is answering at ${baseUrl}. Nothing is listening there, so ${provider} is probably not running${hint ? ` - ${hint}` : ''}`
86
+ : `${provider} request failed: ${String(error)}`);
87
+ }
80
88
  await sleep(2000 * attempt);
81
89
  continue;
82
90
  }
@@ -93,7 +101,7 @@ export function openAiClient({ apiKey, model, reasoningEffort, baseUrl = 'https:
93
101
  }
94
102
  const detail = truncate(await res.text(), 300);
95
103
  if ((res.status !== 429 && res.status < 500) || attempt >= MAX_ATTEMPTS)
96
- throw new Error(`OpenAI API ${res.status}: ${detail}`);
104
+ throw new Error(`${provider} API ${res.status}: ${detail}`);
97
105
  await sleep(2000 * attempt);
98
106
  }
99
107
  },
@@ -0,0 +1,56 @@
1
+ // The JSON a model meant to send, out of whatever it actually sent.
2
+ //
3
+ // OpenAI's `response_format: json_schema` with `strict: true` constrains generation, so the content
4
+ // is a bare JSON object and `JSON.parse` is enough. That is the only provider this was ever run
5
+ // against, and it made the parsing look simpler than it is.
6
+ //
7
+ // Phase 47 put the real client in front of a local OpenAI-compatible server - the path a hospital or
8
+ // a bank has to use, because they cannot send source code to a hosted API - and two of the three
9
+ // commonest local-model replies crashed it:
10
+ //
11
+ // ```json\n{ ... }\n``` SyntaxError: Unexpected token '`'
12
+ // Here is the analysis:\n\n{...} SyntaxError: Unexpected token 'H'
13
+ //
14
+ // Self-hosted Ollama does constrain generation through llama.cpp's grammar decoder, but its own
15
+ // documentation records backends that accept `json_schema` and quietly do not enforce it, and every
16
+ // other local runner - LM Studio, vLLM, llama.cpp's own server - has its own answer. A fence and a
17
+ // sentence of preamble are not a model misbehaving; they are what an unconstrained model does.
18
+ //
19
+ // This is extraction, not repair. Nothing is guessed and nothing is rewritten: the fence comes off,
20
+ // the object between the outermost braces is taken, and anything that still will not parse throws
21
+ // with what actually arrived. A model that replies with prose and no JSON is still an error.
22
+ const FENCED = /^\s*```(?:json|jsonc|JSON)?\s*\n([\s\S]*?)\n?\s*```\s*$/;
23
+ /** Parses a model's reply as JSON, tolerating a fence or a sentence in front of it. */
24
+ export function jsonFromModel(raw) {
25
+ const attempts = [raw, ...candidates(raw)];
26
+ for (const text of attempts) {
27
+ try {
28
+ return JSON.parse(text);
29
+ }
30
+ catch {
31
+ continue;
32
+ }
33
+ }
34
+ throw new Error(`the model did not return JSON: ${raw.trim().slice(0, 200)}${raw.trim().length > 200 ? '…' : ''}`);
35
+ }
36
+ /** The shapes worth trying, in order of how sure we are about them. */
37
+ function candidates(raw) {
38
+ const out = [];
39
+ const fenced = FENCED.exec(raw.trim());
40
+ if (fenced)
41
+ out.push(fenced[1]);
42
+ // The outermost braces, which is what a sentence of preamble leaves behind. Taken from the first
43
+ // `{` to the last `}` rather than by counting: a brace inside a string would defeat the counter,
44
+ // and if the slice is unbalanced `JSON.parse` says so and the next candidate is tried.
45
+ const source = fenced ? fenced[1] : raw;
46
+ const open = source.indexOf('{');
47
+ const close = source.lastIndexOf('}');
48
+ if (open !== -1 && close > open)
49
+ out.push(source.slice(open, close + 1));
50
+ // The same for an array, which `write` and `plan` can both legitimately return.
51
+ const openArray = source.indexOf('[');
52
+ const closeArray = source.lastIndexOf(']');
53
+ if (openArray !== -1 && closeArray > openArray && (open === -1 || openArray < open))
54
+ out.push(source.slice(openArray, closeArray + 1));
55
+ return out;
56
+ }
@@ -4,9 +4,10 @@ import { recordFailure } from './usage.js';
4
4
  import { ROOT_CAUSES } from '../../reporters/types.js';
5
5
  import { truncate } from '../signature.js';
6
6
  import { OUTPUT_SCHEMA, OUTPUT_SCHEMA_NAME, PROMPT_VERSION, SYSTEM_PROMPT } from './prompt.js';
7
+ import { jsonFromModel } from './json-from-model.js';
7
8
  /** Validates the model's JSON. Throws on anything outside the contract. */
8
9
  export function parseLlmOutput(raw) {
9
- const value = JSON.parse(raw);
10
+ const value = jsonFromModel(raw);
10
11
  if (!ROOT_CAUSES.includes(value.category))
11
12
  throw new Error(`category outside the taxonomy: ${String(value.category)}`);
12
13
  if (typeof value.confidence !== 'number' || !Number.isFinite(value.confidence))
@@ -45,7 +45,14 @@ export function modelChoiceFromEnv(purpose) {
45
45
  export function clientFor(choice) {
46
46
  if (choice.provider === 'anthropic')
47
47
  return anthropicClient({ apiKey: choice.apiKey, model: choice.model, baseUrl: choice.baseUrl });
48
- return openAiClient({ apiKey: choice.apiKey || 'local', model: choice.model, reasoningEffort: choice.reasoningEffort, baseUrl: choice.baseUrl });
48
+ return openAiClient({
49
+ apiKey: choice.apiKey || 'local',
50
+ model: choice.model,
51
+ reasoningEffort: choice.reasoningEffort,
52
+ baseUrl: choice.baseUrl,
53
+ provider: choice.provider === 'custom' ? 'the configured endpoint' : choice.provider,
54
+ hint: PROVIDERS[choice.provider].hint,
55
+ });
49
56
  }
50
57
  /** What to tell someone who has no model configured, naming every way to get one. */
51
58
  export function noModelHelp() {
@@ -7,6 +7,7 @@ import { existsSync, readdirSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { readJsonl } from '../../reporters/store.js';
9
9
  import { changeSummary, existingTests, readPages, relatedCodeSources, relatedTestSources } from './grounding.js';
10
+ import { jsonFromModel } from '../llm/json-from-model.js';
10
11
  export const PLAN_PROMPT_VERSION = 'plan-4';
11
12
  /** Failures Tewip already diagnosed in this project (from `tewip explain` runs kept in .tewip). */
12
13
  function failureHistory(root, cap = 60) {
@@ -115,5 +116,5 @@ export function verifyCoverage(plan, facts) {
115
116
  }
116
117
  export async function writePlan(facts, model) {
117
118
  const raw = await model.complete({ system: SYSTEM, user: factsForModel(facts), schemaName: 'tewip_test_plan', schema: SCHEMA });
118
- return verifyCoverage(JSON.parse(raw), facts);
119
+ return verifyCoverage(jsonFromModel(raw), facts);
119
120
  }
@@ -4,6 +4,7 @@
4
4
  // When a draft fails, the model repairs it from the real error - or says the app looks wrong,
5
5
  // because weakening a test until it passes would hide exactly the bug it exists to catch.
6
6
  import { readFileSync } from 'node:fs';
7
+ import { jsonFromModel } from '../llm/json-from-model.js';
7
8
  export const WRITE_PROMPT_VERSION = 'write-6';
8
9
  const RULES = `Rules:
9
10
  - Write in the given framework and language, in the style of the example test: the same imports, helpers, and way of opening pages (if it uses a base URL with relative paths, do the same).
@@ -48,7 +49,7 @@ function contextFor(ctx, scenario) {
48
49
  }
49
50
  export async function draftTest(model, ctx, scenario) {
50
51
  const raw = await model.complete({ system: DRAFT_SYSTEM, user: JSON.stringify(contextFor(ctx, scenario)), schemaName: 'tewip_test_draft', schema: DRAFT_SCHEMA });
51
- return JSON.parse(raw);
52
+ return jsonFromModel(raw);
52
53
  }
53
54
  /** The first lines of a failure: which check failed and how, without the page or the call log. */
54
55
  export function failureHead(error) {
@@ -61,7 +62,7 @@ export async function repairTest(model, ctx, scenario, draft, error, earlier = [
61
62
  const repeated = earlier.length > 0 ? { earlierFailures: earlier, note: 'Your earlier repairs failed too. If the same check keeps failing because the page does something other than the scenario expects, the fault is "app".' } : {};
62
63
  const user = JSON.stringify({ ...contextFor(ctx, scenario), yourTest: draft.code, realError: error.slice(-10000), ...repeated });
63
64
  const raw = await model.complete({ system: REPAIR_SYSTEM, user, schemaName: 'tewip_test_repair', schema: REPAIR_SCHEMA });
64
- return JSON.parse(raw);
65
+ return jsonFromModel(raw);
65
66
  }
66
67
  /**
67
68
  * The broken-on-purpose version of a test, or why none could be made. The piece to change must
@@ -68,6 +68,21 @@ const CHECKS = [
68
68
  // in laravel/framework's SupportStrTest came back as "Tewip would not judge" because the
69
69
  // `assertSame(...)` in them was being read as an unknown helper that might assert.
70
70
  /\$this\s*->\s*assert/,
71
+ // Laravel's fluent assertions, which is what a Laravel HTTP test actually asserts with:
72
+ //
73
+ // $response = $this->get(route('api.v1.autocomplete.accounts'));
74
+ // $response->assertUnauthorized();
75
+ // $response->assertHeader('Content-Type', 'application/json');
76
+ //
77
+ // The pattern above only reads `$this->assert`, so every one of those was an unknown helper that
78
+ // might assert - and firefly-iii, a real finance application, came back **72% "Tewip would not
79
+ // judge"**. A capital after `assert` is what keeps this from matching an ordinary method: it is
80
+ // assertOk, assertStatus, assertJson, assertUnauthorized, never assertion() or asserts().
81
+ // `$response->`, not any `->`: Java's lambda arrow is also `->`, so the first version of this
82
+ // quietly moved thirteen Keycloak verdicts - `coseKey -> assertEcKey(coseKey, ...)` matched. It
83
+ // happened to be right about them, and that is not the point: a PHP fix had changed Java answers
84
+ // nobody had looked at. The corpus caught it on the next run, which is what it is for.
85
+ /\$\w+\s*->\s*assert[A-Z]\w*\s*\(/,
71
86
  /\b(?:self|static|Assert)\s*::\s*assert/,
72
87
  /\bexpect\s*\(.*\)\s*->\s*\w+/,
73
88
  // Rust. A #[should_panic] test asserts by panicking, so it can fail without an assert in it.
@@ -90,7 +105,8 @@ export function checksSomething(text) {
90
105
  */
91
106
  export function unfinishedExpectations(body) {
92
107
  const found = [];
93
- for (const line of body.split(String.fromCharCode(10))) {
108
+ const lines = body.split(String.fromCharCode(10));
109
+ for (const [i, line] of lines.entries()) {
94
110
  const text = line.trim();
95
111
  if (!/^(await\s+)?expect(\.soft)?\s*\(/.test(text))
96
112
  continue;
@@ -98,6 +114,27 @@ export function unfinishedExpectations(body) {
98
114
  if (after === null)
99
115
  continue; // the call spans more lines; judged as part of the whole body
100
116
  const rest = after.trim();
117
+ // An expectation kept in a variable and finished on a later line. RSpec allows it, and
118
+ // discourse writes it:
119
+ //
120
+ // preview =
121
+ // expect(Onebox.preview(url).to_s)
122
+ // preview.to match(/start=120/)
123
+ //
124
+ // `expect(...)` returns the target and `.to` is a method on it, so this is a finished
125
+ // expectation that happens to span three lines. Reading only the line the `expect` is on called
126
+ // it unfinished - "an expectation was written but no matcher was called on it" - about a test
127
+ // that checks two things.
128
+ if (rest === '' || rest === ';') {
129
+ // Only the continuation form, because that is the only one that reaches here: a line reading
130
+ // `preview = expect(x)` does not start with `expect(` and the loop above never looks at it.
131
+ const named = /([A-Za-z_$][\w$]*)\s*=\s*$/.exec(lines[i - 1] ?? '');
132
+ // Built by concatenation on purpose. Written as a template literal, `\b` is a backspace
133
+ // character and `\s` is the letter s, so the pattern silently matched nothing.
134
+ const finished = new RegExp('(^|[^\\w$.])' + named?.[1] + '\\s*\\.\\s*(to|not_to|to_not)\\b');
135
+ if (named && finished.test(body))
136
+ continue;
137
+ }
101
138
  if (rest === '' || rest === ';')
102
139
  found.push(text);
103
140
  // A matcher named but not called: `.toBeVisible;` or `.toBeVisible` at the end of a statement.
@@ -193,7 +193,9 @@ const DECLARES = {
193
193
  rust: /#\s*\[\s*(?:test|tokio::test|async_std::test|rstest)\s*\]/g,
194
194
  // PHPUnit accepts three markers and real suites use all of them: the testFoo naming convention
195
195
  // (laravel/framework does this), the #[Test] attribute of PHPUnit 10, and the @test docblock.
196
- php: /(?:#\s*\[\s*Test[^\]]*\]|\*\s*@test\b|(?:public|protected|private)?\s*function\s+test\w*\s*\()/g,
196
+ // `private`/`protected` dropped: PHPUnit collects public methods only, and a private helper whose
197
+ // name happens to start with "test" is not one. See tests-in.ts for the production file this cost.
198
+ php: /(?:#\s*\[\s*Test[^\]]*\]|\*\s*@test\b|(?:^|[^\w$>])(?:public\s+)?(?:static\s+)?function\s+test\w*\s*\()/gm,
197
199
  };
198
200
  /** JUnit, Kotlin, Scala, Go, C#, PHP and Rust: an annotation or a naming convention, then a braced body. */
199
201
  function bracedTests(language, text) {
@@ -281,6 +283,15 @@ function skipsAtTopLevel(body) {
281
283
  if (lines.length === 0)
282
284
  return false;
283
285
  const base = Math.min(...lines.map((l) => l.length - l.trimStart().length));
284
- return lines.some((l) => l.length - l.trimStart().length === base && /^\s*(?:skip|pending)\b/.test(l));
286
+ // Not a `skip` carrying a trailing condition. Ruby writes a guard as a modifier -
287
+ //
288
+ // skip "suggested topic not found in results" if suggested_topic.nil?
289
+ //
290
+ // - and that test runs every time the condition is false, which is most of the time. The Go side
291
+ // has refused conditional skips since it was written; Ruby's was added in Phase 36 without it, and
292
+ // discourse has four tests that run perfectly well being called "never runs".
293
+ return lines.some((l) => l.length - l.trimStart().length === base &&
294
+ /^\s*(?:skip|pending)\b/.test(l) &&
295
+ !/\s(?:if|unless)\s/.test(l));
285
296
  }
286
297
  export { jsTests, pythonTests, rubyTests, bracedTests };
@@ -30,14 +30,47 @@ export function languageOf(file) {
30
30
  return null;
31
31
  }
32
32
  /** Does this file look like tests at all? Vouching for a source file is a category error. */
33
+ /**
34
+ * Minified or vendored: a file nobody in this project wrote, and nobody is going to fix.
35
+ *
36
+ * firefly-iii ships `public/v1/lib/jquery-4.0.0.min.js`, and minified JavaScript is full of `test(`
37
+ * - it is how a regular expression is applied - so it read as a test file and was judged as one.
38
+ * Nothing useful can come of that: the author cannot act on it, and it is not their code.
39
+ *
40
+ * Length is the signal rather than the name, because plenty of vendored bundles are not called
41
+ * `.min.js`. A minified file is one enormous line; source is not.
42
+ */
43
+ function minifiedOrVendored(file, text) {
44
+ if (/[.-]min\.(js|css|mjs)$/i.test(file))
45
+ return true;
46
+ if (/(^|[\/])(vendor|third[_-]?party|bundles?|dist)[\/]/i.test(file))
47
+ return true;
48
+ const lines = text.split(String.fromCharCode(10));
49
+ const longest = lines.reduce((most, line) => Math.max(most, line.length), 0);
50
+ return longest > 2000 && text.length / Math.max(lines.length, 1) > 400;
51
+ }
33
52
  export function looksLikeTests(file, text) {
53
+ if (minifiedOrVendored(file, text))
54
+ return false;
34
55
  const named = /(^|[\\/])(tests?|spec|specs|__tests__|e2e|it|integration)[\\/]|[._-](test|tests|spec|specs|it)\.[a-z]+$|(^|[\\/])test_[^\\/]+\.py$|_test\.(go|py|rb)$|Tests?\.(java|kt|cs|scala)$/i.test(file);
35
56
  const declares = /\b(it|test|describe|Scenario)\s*\(|^\s*def\s+test_|@Test\b|\[(Test|Fact|Theory)\]|\bfunc\s+Test[A-Z]/m.test(text) ||
36
57
  // Ruby, PHP and Rust, added in Phase 36. PHPUnit names a test `testSomething`, so the `test\s*\(`
37
58
  // above never matched it, and Rust marks one with an attribute no pattern here knew. Both came
38
59
  // back as "0 test(s) read in 0 file(s)", which reads like a clean answer and is not one.
39
60
  /#\s*\[\s*(?:test|tokio::test|rstest)\s*\]/.test(text) ||
40
- /\bfunction\s+test\w*\s*\(|#\s*\[\s*Test\b|\*\s*@test\b/.test(text) ||
61
+ // `public`, or no modifier at all: PHPUnit runs public methods and nothing else. Without that,
62
+ // firefly-iii's `private function testing()` - a middleware helper asking whether the app is in
63
+ // the test environment - made `app/Http/Middleware/InterestingMessage.php` a test file, and the
64
+ // whole of that production class was judged as tests. Vouching for source code is a different
65
+ // question, and this file says so three lines further down.
66
+ // A PHP method called `test*` is not a PHPUnit test. firefly-iii has eight public ones in
67
+ // production controllers - `testRule`, `testNotification` - which are endpoints a user hits to
68
+ // try a rule or send a test notification, and every one of them made its controller a test file.
69
+ // What makes a PHPUnit test is the class it is in, so that is what is asked for: a TestCase to
70
+ // extend, or the attribute and annotation forms that need no base class.
71
+ (/#\s*\[\s*Test\b|\*\s*@test\b/.test(text) ||
72
+ (/(?:^|[^\w$>])(?:public\s+)?(?:static\s+)?function\s+test\w*\s*\(/m.test(text) &&
73
+ /\bextends\s+\w*TestCase\b|\buse\s+PHPUnit\{2}Framework/.test(text))) ||
41
74
  /^\s*(?:RSpec\.)?(?:describe|context)\s+|^\s*(?:x|f)?it\s+["'].*\bdo\b/m.test(text);
42
75
  return named || declares;
43
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tewip-ai",
3
- "version": "0.55.0",
3
+ "version": "0.57.0",
4
4
  "mcpName": "io.github.hmamut39/tewip",
5
5
  "description": "Tewip finds why each failing test failed - real bug, flaky test, environment, test data or changed selector - for any stack, from the command line, CI, or your editor.",
6
6
  "keywords": [