@orangepro/orangepro-mcp 0.1.0 → 0.2.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/dist/local/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs, collectSetupCommands } from "./cliArgs.js";
3
- import { opAnalyze, opAiFlows, opAiLinks, opChanged, opCompare, opDoctor, opDynamicProof, opExplain, opGaps, opGenerate, opBehaviorCoverageHtml, opCoverageReport, opInit, opProveLoop, opRuntimeCoverage, opScore, opRecordRun, opRtm, opStats, opStatus, opUpdate, opSetModelDefault, opStart, getModelDefault, resolveDiffTargets, resolvePrCheckout, writeCompareReport } from "./operations.js";
3
+ import { opAnalyze, opAiFlows, opAiLinks, opChanged, opCompare, opDoctor, opProofDoctor, opDynamicProof, opExplain, opGaps, opGenerate, opBehaviorCoverageHtml, opCoverageReport, opInit, opProveLoop, opRuntimeCoverage, opScore, opRecordRun, opRtm, opStats, opStatus, opUpdate, opSetModelDefault, opStart, getModelDefault, resolveDiffTargets, resolvePrCheckout, writeCompareReport } from "./operations.js";
4
4
  import { dominantBlockReason } from "./viz/behaviorReportData.js";
5
+ import { autoProve, isRoastSurvivor } from "./autoProve.js";
5
6
  import { opRecipeDbSqljs } from "./recipe/dbSqljs.js";
6
7
  import { runExportCli } from "./exportCli.js";
7
8
  import { startLocalMcpServer } from "./mcp.js";
@@ -75,7 +76,8 @@ const HELP = `opro — OrangePro (local-first, BYOK, metadata-only artifacts)
75
76
 
76
77
  Usage:
77
78
  opro # one-command start: analyze, optional AI links + flows, report, RTM, agent handoff
78
- opro start [path] [--base <ref>] [--no-ai] [--no-ai-flows] [--generate-coverage] [--json]
79
+ opro start [path] [--base <ref>] [--no-ai] [--no-ai-flows] [--generate-coverage] [--prompt-version v5] [--json]
80
+ opro roast [path] [--limit 5] [--json] # keyless: find passing tests whose targeted mutant still survives
79
81
  opro init
80
82
  opro setup # interactive: choose a default model provider + model (saved locally)
81
83
  opro analyze [path] [--paths a.csv,b.md] [--include-markdown true|false] [--generate-coverage] [--coverage-timeout-ms 120000] [--ai-flows] [--no-coverage-html] [--json]
@@ -84,7 +86,7 @@ Usage:
84
86
  opro coverage [path] [--generate] [--timeout-ms 120000] [--json]
85
87
  # detects runtime coverage artifacts (Go coverprofile, lcov, coverage.py XML, JaCoCo XML);
86
88
  # --generate runs local free coverage tooling where safely supported (Go, JS/TS scripts, pytest-cov XML, JaCoCo)
87
- opro doctor [--json]
89
+ opro doctor [--proof] [--json] # --proof: why top targets are not Dynamically Proven
88
90
  opro update [--force] [--json]
89
91
  opro changed --base <ref> [--json]
90
92
  opro score [--json]
@@ -92,9 +94,9 @@ Usage:
92
94
  # also returns top_risk_gaps: unproven code symbols ranked by OrangePro Risk Score (P × I × D)
93
95
  opro record --target-symbol sym:file#Symbol [--test path] [--agent-pass true|false] [--evidence-ids id1,id2] [--provider openai] [--model gpt-4.1] [--prompt-version v1] [--json]
94
96
  # record writes static reprove diagnostics only; public Proven requires \`opro prove\`
95
- opro prove --target-symbol sym:file#Symbol --test path --replacement 'return ...;' [--target-file path] [--method name] [--replacement-mode return-json|promise-json] [--runner auto|vitest|jest|mocha] [--link-node-modules] [--json]
97
+ opro prove --target-symbol sym:file#Symbol --test path --replacement 'return ...;' [--target-file path] [--method name] [--replacement-mode return-json|promise-json] [--runner auto|vitest|jest|mocha|pytest] [--link-node-modules] [--json]
96
98
  # runs the dynamic targeted-proof oracle and writes a metadata-only ledger certificate only when baseline-green → mutant assertion-fail closes
97
- opro prove-loop --target-symbol sym:file#Symbol --test path --replacement 'return ...;' [--setup 'npm run build'] [--setup 'npm ci'] [--setup-timeout-ms 120000] [--source path] [--runner auto|vitest|jest|mocha] [--link-node-modules] [--json]
99
+ opro prove-loop --target-symbol sym:file#Symbol --test path --replacement 'return ...;' [--setup 'npm run build'] [--setup 'npm ci'] [--setup-timeout-ms 120000] [--source path] [--runner auto|vitest|jest|mocha|pytest] [--link-node-modules] [--json]
98
100
  # trusted-local wrapper: runs each --setup command in the source checkout, then \`opro prove\` (unchanged oracle + cert), then refreshes the behavior report; setup failure returns unrunnable (never Proven)
99
101
  opro recipe db-sqljs --target-symbol sym:file#Class.method --entity file#Entity --out orangepro_generated/<name>.sqljs.spec.ts [--source path] [--seed-field name] [--json]
100
102
  # writes a REAL NestJS+TypeORM sqljs integration spec (in-memory) + setup profile; makes a DB-backed baseline runnable so \`opro prove-loop\` can close Proven. Never mocks the target.
@@ -166,7 +168,7 @@ async function main() {
166
168
  }
167
169
  // Preload configured tree-sitter grammars before analysis so the sync analyzer
168
170
  // can extract via AST. Idempotent; only the analysis commands pay for it.
169
- if (command === "start" || command === "analyze" || command === "update" || command === "generate" || command === "record" || command === "prove" || command === "prove-loop") {
171
+ if (command === "start" || command === "roast" || command === "analyze" || command === "update" || command === "generate" || command === "record" || command === "prove" || command === "prove-loop") {
170
172
  await preloadTreeSitter(treeSitterLanguages());
171
173
  }
172
174
  switch (command) {
@@ -188,6 +190,7 @@ async function main() {
188
190
  aiFlows: !asBool(flags["no-ai-flows"], false),
189
191
  autoLimit: numericFlag(flags["auto-limit"]),
190
192
  noAuto: asBool(flags["no-auto"], false),
193
+ promptVersion: flags["prompt-version"] === "v5" ? "v5" : undefined,
191
194
  provider: typeof flags.provider === "string" ? flags.provider : undefined,
192
195
  model: typeof flags.model === "string" ? flags.model : undefined
193
196
  });
@@ -274,6 +277,66 @@ async function main() {
274
277
  }
275
278
  return 0;
276
279
  }
280
+ case "roast": {
281
+ const source = positionals[0] || ".";
282
+ const limit = numericFlag(flags.limit) ?? numericFlag(flags["auto-limit"]) ?? 5;
283
+ const clearProgress = !json ? installCliProgress("roast") : () => undefined;
284
+ let res;
285
+ try {
286
+ opAnalyze(cwd, { source });
287
+ res = await autoProve(cwd, { autoLimit: limit, existingOnly: true }, { clock: () => new Date().toISOString(), env: process.env, proveLoop: opProveLoop });
288
+ }
289
+ finally {
290
+ clearProgress();
291
+ }
292
+ const survived = res.attempts.filter(isRoastSurvivor);
293
+ const payload = {
294
+ status: "roast",
295
+ source,
296
+ attempted: res.attempted,
297
+ dynamically_proven: res.proven,
298
+ survived_mutants: survived.length,
299
+ needs_setup: res.needs_setup,
300
+ survivors: survived.map((a) => ({
301
+ target_symbol: a.target_symbol,
302
+ test_path: a.test_path,
303
+ mutant_status: a.mutant_status,
304
+ reason: a.reason
305
+ }))
306
+ };
307
+ if (json) {
308
+ printJson(payload);
309
+ }
310
+ else {
311
+ out("OrangePro roast complete.");
312
+ out(` attempted: ${payload.attempted}`);
313
+ out(` Newly proven this run: ${payload.dynamically_proven}`);
314
+ out(` Survived mutants: ${payload.survived_mutants}`);
315
+ if (survived.length > 0) {
316
+ out("");
317
+ out(`${survived.length} passing test(s) still passed when OrangePro replaced the target with a sentinel mutant:`);
318
+ for (const attempt of survived.slice(0, limit)) {
319
+ out(` - ${attempt.test_path} survived mutant on ${attempt.target_symbol}`);
320
+ if (attempt.reason)
321
+ out(` reason: ${attempt.reason}`);
322
+ }
323
+ out("");
324
+ out("Equivalent mutants can also survive, so treat this as a proof-strengthening queue, not automatic blame.");
325
+ out("Survived mutants are not Dynamically Proven. They are targets for stronger assertions.");
326
+ }
327
+ else if (res.proven > 0) {
328
+ out(" verdict: no survived targeted mutants found in this pass.");
329
+ }
330
+ else if (res.needs_setup.length > 0) {
331
+ const dom = dominantBlockReason(res.needs_setup);
332
+ out(` verdict: proof could not run for ${res.needs_setup.length} target(s)${dom ? `; dominant blocker: ${dom.label}` : ""}.`);
333
+ }
334
+ else {
335
+ out(" verdict: no survived targeted mutants found in this pass.");
336
+ }
337
+ }
338
+ return 0;
339
+ }
277
340
  case "init": {
278
341
  const res = opInit(cwd);
279
342
  if (json)
@@ -498,6 +561,24 @@ async function main() {
498
561
  return 0;
499
562
  }
500
563
  case "doctor": {
564
+ if (asBool(flags.proof, false)) {
565
+ const res = opProofDoctor(cwd);
566
+ if (json)
567
+ printJson(res);
568
+ else {
569
+ out(res.headline);
570
+ for (const b of res.blockers) {
571
+ out(` ${b.count} target${b.count === 1 ? "" : "s"} blocked by ${b.label}${b.source === "preflight" ? " (preflight — no test was run)" : ""}`);
572
+ out(` e.g. ${b.representative.target_symbol}${b.representative.reason ? ` — ${b.representative.reason}` : ""}`);
573
+ out(` next: ${b.next_step}`);
574
+ }
575
+ for (const nk of res.non_killing) {
576
+ out(` mutant survived: ${nk.target_symbol}${nk.test_path ? ` (test: ${nk.test_path})` : ""}`);
577
+ out(` ${nk.note}`);
578
+ }
579
+ }
580
+ return 0;
581
+ }
501
582
  const res = opDoctor(cwd);
502
583
  if (json)
503
584
  printJson(res);
@@ -642,15 +723,16 @@ async function main() {
642
723
  if (flags["replacement-mode"] !== undefined && !replacementMode) {
643
724
  throw new Error("--replacement-mode must be one of: return-json, promise-json");
644
725
  }
645
- const proofRunner = flags.runner === undefined ? undefined : flags.runner === "auto" || flags.runner === "vitest" || flags.runner === "jest" || flags.runner === "mocha" ? flags.runner : undefined;
726
+ const proofRunner = flags.runner === undefined ? undefined : flags.runner === "auto" || flags.runner === "vitest" || flags.runner === "jest" || flags.runner === "mocha" || flags.runner === "pytest" ? flags.runner : undefined;
646
727
  if (flags.runner !== undefined && !proofRunner) {
647
- throw new Error("--runner must be one of: auto, vitest, jest, mocha");
728
+ throw new Error("--runner must be one of: auto, vitest, jest, mocha, pytest");
648
729
  }
649
730
  const res = opDynamicProof(cwd, {
650
731
  target_symbol: typeof flags["target-symbol"] === "string" ? flags["target-symbol"] : undefined,
651
732
  target_id: typeof flags.target === "string" ? flags.target : undefined,
652
733
  source: typeof flags.source === "string" ? flags.source : undefined,
653
734
  test_path: typeof flags.test === "string" ? flags.test : "",
735
+ test_run: typeof flags["test-run"] === "string" ? flags["test-run"] : undefined,
654
736
  target_path: typeof flags["target-file"] === "string" ? flags["target-file"] : undefined,
655
737
  method: typeof flags.method === "string" ? flags.method : undefined,
656
738
  replacement: typeof flags.replacement === "string" ? flags.replacement : "",
@@ -686,15 +768,16 @@ async function main() {
686
768
  if (flags["replacement-mode"] !== undefined && !replacementMode) {
687
769
  throw new Error("--replacement-mode must be one of: return-json, promise-json");
688
770
  }
689
- const proofRunner = flags.runner === undefined ? undefined : flags.runner === "auto" || flags.runner === "vitest" || flags.runner === "jest" || flags.runner === "mocha" ? flags.runner : undefined;
771
+ const proofRunner = flags.runner === undefined ? undefined : flags.runner === "auto" || flags.runner === "vitest" || flags.runner === "jest" || flags.runner === "mocha" || flags.runner === "pytest" ? flags.runner : undefined;
690
772
  if (flags.runner !== undefined && !proofRunner) {
691
- throw new Error("--runner must be one of: auto, vitest, jest, mocha");
773
+ throw new Error("--runner must be one of: auto, vitest, jest, mocha, pytest");
692
774
  }
693
775
  const res = opProveLoop(cwd, {
694
776
  target_symbol: typeof flags["target-symbol"] === "string" ? flags["target-symbol"] : undefined,
695
777
  target_id: typeof flags.target === "string" ? flags.target : undefined,
696
778
  source: typeof flags.source === "string" ? flags.source : undefined,
697
779
  test_path: typeof flags.test === "string" ? flags.test : "",
780
+ test_run: typeof flags["test-run"] === "string" ? flags["test-run"] : undefined,
698
781
  target_path: typeof flags["target-file"] === "string" ? flags["target-file"] : undefined,
699
782
  method: typeof flags.method === "string" ? flags.method : undefined,
700
783
  replacement: typeof flags.replacement === "string" ? flags.replacement : "",
@@ -49,6 +49,7 @@ export const VALUE_FLAGS = new Set([
49
49
  "target-symbol",
50
50
  "test",
51
51
  "test-env",
52
+ "test-run",
52
53
  "vitest-config",
53
54
  "timeout-ms"
54
55
  ]);
@@ -209,13 +209,18 @@ function runnerForDynamicProof(framework) {
209
209
  return "jest";
210
210
  if (f.includes("mocha"))
211
211
  return "mocha";
212
+ if (f.includes("pytest") || f.includes("python"))
213
+ return "pytest";
212
214
  return undefined;
213
215
  }
214
216
  function symbolFile(target) {
215
217
  return target.slice("sym:".length).split("#")[0] ?? "";
216
218
  }
217
219
  function supportsDynamicProof(target) {
218
- return /\.[cm]?[jt]sx?$/i.test(symbolFile(target));
220
+ return /\.(?:[cm]?[jt]sx?|py)$/i.test(symbolFile(target));
221
+ }
222
+ function replacementForDynamicProof(target) {
223
+ return /\.py$/i.test(symbolFile(target)) ? "return 0" : "return null;";
219
224
  }
220
225
  export function runHintsFor(tests, repoRoot, startIndex = 0) {
221
226
  return tests.map((t, i) => {
@@ -236,13 +241,13 @@ export function runHintsFor(tests, repoRoot, startIndex = 0) {
236
241
  args: {
237
242
  target_symbol: target,
238
243
  test_path: suggested_path,
239
- replacement: "return null;",
244
+ replacement: replacementForDynamicProof(target),
240
245
  ...(runnerForDynamicProof(t.framework_hint) ? { runner: runnerForDynamicProof(t.framework_hint) } : {})
241
246
  }
242
247
  }
243
248
  }
244
249
  : {
245
- handoff_note: "Dynamic public Proven currently supports TS/JS CodeSymbol targets only; use record_run for static diagnostics."
250
+ handoff_note: "Dynamic public Proven currently supports TS/JS/Python CodeSymbol targets only; use record_run for static diagnostics."
246
251
  }),
247
252
  record_run: {
248
253
  tool: "orangepro_record_run",
@@ -285,7 +290,7 @@ export const AGENT_RUN_WORKFLOW = [
285
290
  "OrangePro returns the test code; YOUR agent runs it. Using your shell tools:",
286
291
  "1. Write each test's `body` to its `suggested_path` in the repo.",
287
292
  "2. Run its `run_command` from the package that owns the test's directory (monorepos: cd into that package first so the framework's module roots resolve) — or use the repo's own test command (npm test, pytest, npx playwright test).",
288
- "3. For public Proven, call `prove_run`/`orangepro_prove` after the test passes; it reruns baseline + sentinel-mutant and closes only on a dynamic assertion kill. `record_run` is static diagnostics only.",
293
+ "3. For public Proven, call `orangepro_prove` with the returned `prove_run` args after the test passes; it reruns baseline + sentinel-mutant and closes only on a dynamic assertion kill. `record_run` is static diagnostics only.",
289
294
  "4. Report pass/fail, error messages, and stack traces to the developer; propose a fix for failures.",
290
295
  "Runnable Python and Go drafts require local validation tools on PATH (`python3` for pytest syntax checks, `gofmt` for Go syntax checks). If those tools are missing, OrangePro safely returns drafts without run commands.",
291
296
  "Requires the repo's test framework + dependencies to already be installed. If a test fails to run for environment reasons (missing framework/deps), that's a local setup issue, not the generated test.",
@@ -79,6 +79,7 @@ export function makeEdge(input) {
79
79
  }
80
80
  export function makeProofEdges(input) {
81
81
  const testExternalId = `test:${input.testRel}`;
82
+ const props = input.properties ? { properties: input.properties } : {};
82
83
  return [
83
84
  makeEdge({
84
85
  from_external_id: input.symId,
@@ -87,7 +88,8 @@ export function makeProofEdges(input) {
87
88
  evidence_strength: "hard",
88
89
  review_status: "auto_detected",
89
90
  provenance: input.provenance,
90
- last_verified: input.lastVerified
91
+ last_verified: input.lastVerified,
92
+ ...props
91
93
  }),
92
94
  makeEdge({
93
95
  from_external_id: testExternalId,
@@ -96,7 +98,8 @@ export function makeProofEdges(input) {
96
98
  evidence_strength: "hard",
97
99
  review_status: "auto_detected",
98
100
  provenance: input.provenance,
99
- last_verified: input.lastVerified
101
+ last_verified: input.lastVerified,
102
+ ...props
100
103
  })
101
104
  ];
102
105
  }
@@ -48,7 +48,7 @@ export function targetLanguage(symExtId) {
48
48
  return ext || "unknown";
49
49
  }
50
50
  export function canReproveLanguage(language) {
51
- return language === "typescript" || language === "go";
51
+ return language === "typescript" || language === "go" || language === "python";
52
52
  }
53
53
  export function loadLedger(root) {
54
54
  const path = ledgerPath(root);
package/dist/local/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { z } from "zod";
4
- import { opAnalyze, opAiFlows, opAiLinks, opChanged, opCompare, opDoctor, opDynamicProof, opExplain, opExport, opGaps, opGenerate, opProveLoop, opRecordRun, opRtm, opScore, opStats, opStart, opStatus, opUpdate, resolveDiffTargets } from "./operations.js";
4
+ import { opAnalyze, opAiFlows, opAiLinks, opChanged, opCompare, opDoctor, opProofDoctor, opDynamicProof, opExplain, opExport, opGaps, opGenerate, opProveLoop, opRecordRun, opRtm, opScore, opStats, opStart, opStatus, opUpdate, resolveDiffTargets } from "./operations.js";
5
5
  import { runnableRunHintsFor, AGENT_RUN_WORKFLOW, GROUNDING_CONTRACT } from "./generate/runHints.js";
6
6
  import { preloadTreeSitter } from "./analyze/treeSitter/engine.js";
7
7
  import { treeSitterLanguages } from "./analyze/treeSitter/languages.js";
@@ -22,11 +22,11 @@ const DEFAULT_MARKDOWN_RTM_LIMIT = 500;
22
22
  * Codex, Claude, Copilot, …). First slice exposes NO upload or repo-write tools.
23
23
  */
24
24
  export function createLocalServer() {
25
- const server = new McpServer({ name: "orangepro-local", version: "0.1.0" });
25
+ const server = new McpServer({ name: "orangepro-local", version: "0.2.0" });
26
26
  const root = (ws) => ws || process.cwd();
27
27
  server.registerTool("orangepro_start", {
28
28
  title: "Start OrangePro",
29
- description: "One-command local setup for a repo or PR: analyze sources, auto-apply weak AI candidate links and candidate flows when a real BYOK provider is configured, write graph.html + rtm.md, summarize changed/gap targets, and return agent next actions. AI lanes stay separate and never affect Proven coverage.",
29
+ description: "One-command local setup for a repo or PR: analyze sources, auto-apply weak AI candidate links and candidate flows when a real BYOK provider is configured, write behavior-coverage.html + rtm.md, summarize changed/gap targets, and return agent next actions. AI lanes stay separate and never affect Proven coverage.",
30
30
  inputSchema: {
31
31
  ...Workspace,
32
32
  source: z.string().optional().describe("Source path to analyze. Defaults to workspace."),
@@ -38,7 +38,8 @@ export function createLocalServer() {
38
38
  no_ai_flows: z.boolean().optional().describe("Disable automatic AI candidate-flow discovery while keeping weak AI links enabled."),
39
39
  ai_all: z.boolean().optional().describe("Run AI weak-linking over all deterministic behavior nodes instead of gaps-only."),
40
40
  provider: z.enum(["openai", "ollama", "anthropic"]).optional().describe("BYOK provider override for the AI passes."),
41
- model: z.string().optional().describe("Model name override.")
41
+ model: z.string().optional().describe("Model name override."),
42
+ prompt_version: z.enum(["v2", "v5"]).optional().describe("Opt-in generation strategy for the auto-prove generation lane. Default v2/deterministic; v5 uses batched two-phase generation. The prove/mint gate is unchanged either way.")
42
43
  },
43
44
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
44
45
  }, async (input) => {
@@ -54,7 +55,8 @@ export function createLocalServer() {
54
55
  aiAll: input.ai_all,
55
56
  aiFlows: !input.no_ai_flows,
56
57
  provider: input.provider,
57
- model: input.model
58
+ model: input.model,
59
+ promptVersion: input.prompt_version
58
60
  }));
59
61
  }
60
62
  catch (error) {
@@ -124,11 +126,18 @@ export function createLocalServer() {
124
126
  server.registerTool("orangepro_doctor", {
125
127
  title: "Recommend next evidence",
126
128
  description: "Recommend the smallest next source that would most improve generated-test quality, with expected score impact. Teaches what data improves output.",
127
- inputSchema: { ...Workspace, goal: z.string().optional().describe("Optimization goal hint, e.g. 'better_tests'.") },
129
+ inputSchema: {
130
+ ...Workspace,
131
+ goal: z.string().optional().describe("Optimization goal hint, e.g. 'better_tests'."),
132
+ proof: z
133
+ .boolean()
134
+ .optional()
135
+ .describe("Proof-focused mode: explain why top targets are not Dynamically Proven (deduped blockers + smallest next steps). Read-only; mints nothing.")
136
+ },
128
137
  annotations: { readOnlyHint: true, destructiveHint: false }
129
138
  }, async (input) => {
130
139
  try {
131
- return asText(opDoctor(root(input.workspace)));
140
+ return asText(input.proof ? opProofDoctor(root(input.workspace)) : opDoctor(root(input.workspace)));
132
141
  }
133
142
  catch (error) {
134
143
  return asError(error);
@@ -198,12 +207,13 @@ export function createLocalServer() {
198
207
  target_symbol: z.string().optional().describe("Exact CodeSymbol external id, e.g. sym:src/service.ts#OrderService.createOrder. Preferred."),
199
208
  target_id: z.string().optional().describe("Optional graph target id that must resolve to exactly one hard-linked CodeSymbol."),
200
209
  source: z.string().optional().describe("Source checkout containing the test and target files. Defaults to workspace."),
201
- test_path: z.string().describe("Repo-relative test file to run."),
202
- replacement: z.string().describe("Inert sentinel body, e.g. 'return null;' or 'return {\"ok\":false};'. Must be JSON-only per the oracle."),
210
+ test_path: z.string().optional().describe("TS/JS: repo-relative test file to run (required for TS/JS targets)."),
211
+ test_run: z.string().optional().describe("Go: fully-anchored test name for `go test -run`, e.g. '^TestCompute$' (required for Go targets). Java: 'TestClass#testMethod', e.g. 'CalculatorTest#addsTwoNumbers' (required for Java targets)."),
212
+ replacement: z.string().optional().describe("TS/JS: inert sentinel body, e.g. 'return null;' or 'return {\"ok\":false};'. Must be JSON-only per the oracle (required for TS/JS targets). Go derives its own sentinel."),
203
213
  target_file: z.string().optional().describe("Optional consistency check. If provided, must match the file derived from target_symbol."),
204
214
  method: z.string().optional().describe("Optional consistency check. If provided, must match the member derived from target_symbol."),
205
215
  replacement_mode: z.enum(["return-json", "promise-json"]).optional().describe("Sentinel wrapping mode. Default return-json."),
206
- runner: z.enum(["auto", "vitest", "jest", "mocha"]).optional().describe("Test runner override. Default auto."),
216
+ runner: z.enum(["auto", "vitest", "jest", "mocha", "pytest"]).optional().describe("Test runner override. Default auto."),
207
217
  timeout_ms: z.number().int().min(1000).max(600000).optional().describe("Per baseline/mutant test timeout."),
208
218
  link_node_modules: z.boolean().optional().describe("Trusted-repo speed mode: symlink node_modules into temp copies."),
209
219
  vitest_config: z.string().optional().describe("Repo-relative Vitest config path."),
@@ -220,6 +230,7 @@ export function createLocalServer() {
220
230
  target_id: input.target_id,
221
231
  source: input.source,
222
232
  test_path: input.test_path,
233
+ test_run: input.test_run,
223
234
  target_path: input.target_file,
224
235
  method: input.method,
225
236
  replacement: input.replacement,
@@ -245,12 +256,13 @@ export function createLocalServer() {
245
256
  target_symbol: z.string().optional().describe("Exact CodeSymbol external id, e.g. sym:src/service.ts#OrderService.createOrder. Preferred."),
246
257
  target_id: z.string().optional().describe("Optional graph target id that must resolve to exactly one hard-linked CodeSymbol."),
247
258
  source: z.string().optional().describe("Source checkout containing the test and target files. Setup runs here. Defaults to workspace."),
248
- test_path: z.string().describe("Repo-relative test file to run."),
249
- replacement: z.string().describe("Inert sentinel body, e.g. 'return null;' or 'return {\"ok\":false};'. Must be JSON-only per the oracle."),
259
+ test_path: z.string().optional().describe("TS/JS: repo-relative test file to run (required for TS/JS targets)."),
260
+ test_run: z.string().optional().describe("Go: fully-anchored test name for `go test -run`, e.g. '^TestCompute$' (required for Go targets). Java: 'TestClass#testMethod', e.g. 'CalculatorTest#addsTwoNumbers' (required for Java targets)."),
261
+ replacement: z.string().optional().describe("TS/JS: inert sentinel body, e.g. 'return null;' or 'return {\"ok\":false};'. Must be JSON-only per the oracle (required for TS/JS targets). Go derives its own sentinel."),
250
262
  target_file: z.string().optional().describe("Optional consistency check. If provided, must match the file derived from target_symbol."),
251
263
  method: z.string().optional().describe("Optional consistency check. If provided, must match the member derived from target_symbol."),
252
264
  replacement_mode: z.enum(["return-json", "promise-json"]).optional().describe("Sentinel wrapping mode. Default return-json."),
253
- runner: z.enum(["auto", "vitest", "jest", "mocha"]).optional().describe("Test runner override. Default auto."),
265
+ runner: z.enum(["auto", "vitest", "jest", "mocha", "pytest"]).optional().describe("Test runner override. Default auto."),
254
266
  timeout_ms: z.number().int().min(1000).max(600000).optional().describe("Per baseline/mutant test timeout."),
255
267
  link_node_modules: z.boolean().optional().describe("Trusted-repo speed mode: symlink node_modules into temp copies."),
256
268
  vitest_config: z.string().optional().describe("Repo-relative Vitest config path."),
@@ -275,6 +287,7 @@ export function createLocalServer() {
275
287
  target_id: input.target_id,
276
288
  source: input.source,
277
289
  test_path: input.test_path,
290
+ test_run: input.test_run,
278
291
  target_path: input.target_file,
279
292
  method: input.method,
280
293
  replacement: input.replacement,