opencode-swarm 6.81.1 → 6.82.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/dist/index.js CHANGED
@@ -58,6 +58,7 @@ var init_tool_names = __esm(() => {
58
58
  "test_runner",
59
59
  "test_impact",
60
60
  "mutation_test",
61
+ "generate_mutants",
61
62
  "detect_domains",
62
63
  "gitingest",
63
64
  "retrieve_summary",
@@ -69,6 +70,7 @@ var init_tool_names = __esm(() => {
69
70
  "write_retro",
70
71
  "write_drift_evidence",
71
72
  "write_hallucination_evidence",
73
+ "write_mutation_evidence",
72
74
  "declare_scope",
73
75
  "knowledge_query",
74
76
  "doc_scan",
@@ -216,6 +218,8 @@ var init_constants = __esm(() => {
216
218
  "test_runner",
217
219
  "test_impact",
218
220
  "mutation_test",
221
+ "generate_mutants",
222
+ "write_mutation_evidence",
219
223
  "todo_extract",
220
224
  "update_task_status",
221
225
  "lint_spec",
@@ -415,6 +419,8 @@ var init_constants = __esm(() => {
415
419
  test_runner: "auto-detect and run tests",
416
420
  test_impact: "identify test files impacted by changed source files via import analysis",
417
421
  mutation_test: "executes pre-generated mutation patches against tests, evaluates kill rate against quality gate thresholds",
422
+ generate_mutants: "generate LLM-based mutation testing patches for source files; returns MutationPatch[] for direct consumption by the mutation_test tool",
423
+ write_mutation_evidence: "write mutation gate evidence for a completed phase; normalizes PASS/WARN/FAIL/SKIP verdicts and writes .swarm/evidence/{phase}/mutation-gate.json",
418
424
  pkg_audit: "dependency vulnerability scan \u2014 npm/pip/cargo",
419
425
  complexity_hotspots: "git churn \xD7 complexity risk map",
420
426
  schema_drift: "OpenAPI spec vs route drift",
@@ -453,8 +459,8 @@ var init_constants = __esm(() => {
453
459
  lint_spec: "validate .swarm/spec.md format and required fields",
454
460
  get_approved_plan: "retrieve the last critic-approved immutable plan snapshot for baseline drift comparison",
455
461
  repo_map: "query the repo code graph: importers, dependencies, blast radius, and localization context for structural awareness before refactoring",
456
- get_qa_gate_profile: "retrieve the QA gate profile for the current plan: gates, lock state, and profile hash. Read-only.",
457
- set_qa_gates: "configure the QA gate profile for the current plan. Architect-only. Ratchet-tighter only \u2014 rejected once the profile is locked after critic approval.",
462
+ get_qa_gate_profile: "retrieve the QA gate profile for the current plan: gates (reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test), lock state, and profile hash. Read-only.",
463
+ set_qa_gates: "configure the QA gate profile for the current plan. Architect-only. Ratchet-tighter only \u2014 rejected once the profile is locked after critic approval. Supports: reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test.",
458
464
  req_coverage: "query requirement coverage status for tracked functional requirements"
459
465
  };
460
466
  for (const [agentName, tools] of Object.entries(AGENT_TOOL_MAP)) {
@@ -19701,7 +19707,8 @@ var init_qa_gate_profile = __esm(() => {
19701
19707
  sme_enabled: true,
19702
19708
  critic_pre_plan: true,
19703
19709
  hallucination_guard: false,
19704
- sast_enabled: true
19710
+ sast_enabled: true,
19711
+ mutation_test: false
19705
19712
  };
19706
19713
  });
19707
19714
 
@@ -23538,7 +23545,7 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
23538
23545
  const planMdPath = path9.resolve(effectiveDirectory, ".swarm", "plan.md").toLowerCase();
23539
23546
  const planJsonPath = path9.resolve(effectiveDirectory, ".swarm", "plan.json").toLowerCase();
23540
23547
  if (resolvedTarget === planMdPath || resolvedTarget === planJsonPath) {
23541
- throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use update_task_status() to mark tasks complete, " + "phase_complete() for phase transitions, or " + "save_plan to create/restructure plans.");
23548
+ throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use save_plan for ALL structural plan changes (adding/removing tasks, updating descriptions, dependencies, or phase names). " + "Use update_task_status() for task status only. " + "Use phase_complete() for phase transitions only.");
23542
23549
  }
23543
23550
  }
23544
23551
  if (!targetPath && (tool === "apply_patch" || tool === "patch")) {
@@ -23547,7 +23554,7 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
23547
23554
  const planMdPath = path9.resolve(effectiveDirectory, ".swarm", "plan.md").toLowerCase();
23548
23555
  const planJsonPath = path9.resolve(effectiveDirectory, ".swarm", "plan.json").toLowerCase();
23549
23556
  if (resolvedP.toLowerCase() === planMdPath || resolvedP.toLowerCase() === planJsonPath) {
23550
- throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use update_task_status() to mark tasks complete, " + "phase_complete() for phase transitions, or " + "save_plan to create/restructure plans.");
23557
+ throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use save_plan for ALL structural plan changes (adding/removing tasks, updating descriptions, dependencies, or phase names). " + "Use update_task_status() for task status only. " + "Use phase_complete() for phase transitions only.");
23551
23558
  }
23552
23559
  if (isOutsideSwarmDir(p, effectiveDirectory) && (isSourceCodePath(p) || hasTraversalSegments(p))) {
23553
23560
  const session = swarmState.agentSessions.get(sessionID);
@@ -52104,7 +52111,8 @@ var init_qa_gates = __esm(() => {
52104
52111
  "sme_enabled",
52105
52112
  "critic_pre_plan",
52106
52113
  "hallucination_guard",
52107
- "sast_enabled"
52114
+ "sast_enabled",
52115
+ "mutation_test"
52108
52116
  ];
52109
52117
  });
52110
52118
 
@@ -53889,7 +53897,7 @@ var init_registry = __esm(() => {
53889
53897
  handler: (ctx) => handleQaGatesCommand(ctx.directory, ctx.args, ctx.sessionID),
53890
53898
  description: "View or modify QA gate profile for the current plan [enable|override <gate>...]",
53891
53899
  args: "[show|enable|override] <gate>...",
53892
- details: "show: display spec-level, session-override, and effective QA gates for the current plan. enable: persist gate(s) into the locked-once profile (architect; rejected after critic approval lock). override: session-only ratchet-tighter enable. Valid gates: reviewer, test_engineer, council_mode, sme_enabled, critic_pre_plan, hallucination_guard, sast_enabled."
53900
+ details: "show: display spec-level, session-override, and effective QA gates for the current plan. enable: persist gate(s) into the locked-once profile (architect; rejected after critic approval lock). override: session-only ratchet-tighter enable. Valid gates: reviewer, test_engineer, council_mode, sme_enabled, critic_pre_plan, hallucination_guard, sast_enabled, mutation_test."
53893
53901
  },
53894
53902
  promote: {
53895
53903
  handler: (ctx) => handlePromoteCommand(ctx.directory, ctx.args),
@@ -54052,7 +54060,7 @@ function buildQaGateSelectionDialogue(modeLabel) {
54052
54060
  const leadIn = modeLabel === "BRAINSTORM" ? "Now ask the user which QA gates to enable for this plan \u2014 do not select on their behalf." : modeLabel === "SPECIFY" ? "Ask the user which QA gates to enable for this plan before suggesting the next step." : "No pending gate selection found in `.swarm/context.md`. Ask the user inline now.";
54053
54061
  return `${leadIn}
54054
54062
 
54055
- Present the seven gates with their defaults (DEFAULT_QA_GATES) as a single user-facing question. Offer the user a one-shot choice: accept defaults, or customize. The seven gates are:
54063
+ Present the eight gates with their defaults (DEFAULT_QA_GATES) as a single user-facing question. Offer the user a one-shot choice: accept defaults, or customize. The eight gates are:
54056
54064
  - reviewer (default: ON) \u2014 code review of coder output
54057
54065
  - test_engineer (default: ON) \u2014 test verification of coder output
54058
54066
  - sme_enabled (default: ON) \u2014 SME consultation during planning/clarification
@@ -54060,6 +54068,7 @@ Present the seven gates with their defaults (DEFAULT_QA_GATES) as a single user-
54060
54068
  - sast_enabled (default: ON) \u2014 static security scanning
54061
54069
  - council_mode (default: OFF) \u2014 multi-member council gate (recommended for high-impact architecture, public APIs, schema/data mutation, security-sensitive code)
54062
54070
  - hallucination_guard (default: OFF) \u2014 when enabled, mandatory per-phase API/signature/claim/citation verification via critic_hallucination_verifier at PHASE-WRAP; phase_complete will REJECT phase completion unless .swarm/evidence/{phase}/hallucination-guard.json exists with an APPROVED verdict (recommended for claim-heavy or research-heavy work)
54071
+ - mutation_test (default: OFF) \u2014 when enabled, runs mutation testing on source files touched this phase via generate_mutants + mutation_test + write_mutation_evidence at PHASE-WRAP; FAIL verdict blocks phase_complete; WARN is non-blocking (recommended for projects with coverage gaps or safety-critical code)
54063
54072
 
54064
54073
  One question, one message, defaults pre-stated. Wait for the user's answer.`;
54065
54074
  }
@@ -54512,7 +54521,7 @@ The correct tools: save_plan to create or restructure a plan (writes plan.json \
54512
54521
  .swarm/plan.md and .swarm/plan.json are READABLE but NOT DIRECTLY WRITABLE for state transitions.
54513
54522
  Task-level status changes (marking individual tasks as "completed") must use update_task_status().
54514
54523
  Phase-level completion (marking an entire phase as done) must use phase_complete().
54515
- You may write to plan.md/plan.json for STRUCTURAL changes (adding tasks, updating descriptions).
54524
+ For STRUCTURAL changes (adding tasks, updating descriptions, changing dependencies), use save_plan \u2014 do NOT write plan.md/plan.json directly.
54516
54525
  You may NOT write to plan.md/plan.json to change task completion status or phase status directly.
54517
54526
  "I'll just mark it done directly" is a bypass \u2014 equivalent to GATE_DELEGATION_BYPASS.
54518
54527
 
@@ -54762,6 +54771,7 @@ Do NOT call \`set_qa_gates\` yet \u2014 \`plan.json\` does not exist at this poi
54762
54771
  - sast_enabled: <true|false>
54763
54772
  - council_mode: <true|false>
54764
54773
  - hallucination_guard: <true|false>
54774
+ - mutation_test: <true|false>
54765
54775
  - recorded_at: <ISO timestamp>
54766
54776
  \`\`\`
54767
54777
  MODE: PLAN applies these after \`save_plan\` succeeds via \`set_qa_gates\`.
@@ -54814,6 +54824,7 @@ Do NOT call \`set_qa_gates\` yet \u2014 \`plan.json\` does not exist at this poi
54814
54824
  - sast_enabled: <true|false>
54815
54825
  - council_mode: <true|false>
54816
54826
  - hallucination_guard: <true|false>
54827
+ - mutation_test: <true|false>
54817
54828
  - recorded_at: <ISO timestamp>
54818
54829
  \`\`\`
54819
54830
  MODE: PLAN will read this section after \`save_plan\` succeeds and persist via \`set_qa_gates\`.
@@ -55388,12 +55399,23 @@ The tool will automatically write the retrospective to \`.swarm/evidence/retro-{
55388
55399
  After the delegation returns APPROVED, YOU (the architect) call the \`write_hallucination_evidence\` tool to write the evidence artifact (phase, verdict, summary). The critic does NOT write files \u2014 it is read-only.
55389
55400
  NOTE: This step is enforced by the plugin. If \`hallucination_guard\` is enabled and \`.swarm/evidence/{phase}/hallucination-guard.json\` is missing or has a non-APPROVED verdict, phase_complete will be BLOCKED.
55390
55401
  PROFILE LOCK NOTE: If the QA gate profile is already locked (drift verification has approved the plan) and \`hallucination_guard\` was not elected during the initial QA GATE SELECTION, this step is skipped \u2014 report the skip to the user. A new plan cycle is required to enable the gate.
55402
+ 5.56. **Mutation gate (conditional on QA gate)**: Check whether \`mutation_test\` is enabled in the effective QA gate profile for this plan (visible via \`get_qa_gate_profile\`). If disabled or turbo mode is active, skip silently and proceed to step 5.6.
55403
+ If \`mutation_test\` is enabled:
55404
+ 1. Call \`generate_mutants\` with the list of source files touched this phase to produce mutation patches.
55405
+ 2. If \`generate_mutants\` returns a SKIP verdict (LLM unavailable), call \`write_mutation_evidence\` with verdict SKIP and proceed \u2014 SKIP does not block.
55406
+ 3. Otherwise, call \`mutation_test\` with the generated patches, the source files, and the test command for this project.
55407
+ 4. Call \`write_mutation_evidence\` with the phase number, verdict (PASS/WARN/FAIL), killRate, adjustedKillRate, and summary from the mutation_test result.
55408
+ 5. If verdict is FAIL: STOP \u2014 do NOT call phase_complete. Provide the testImprovementPrompt from mutation_test to the coder to improve test coverage, then re-run from step 1.
55409
+ 6. If verdict is WARN: non-blocking \u2014 proceed to step 5.6 with a warning to the user.
55410
+ 7. If verdict is PASS: proceed to step 5.6.
55411
+ NOTE: This step is enforced by the plugin. If \`mutation_test\` is enabled and \`.swarm/evidence/{phase}/mutation-gate.json\` is missing or has a 'fail' verdict, phase_complete will be BLOCKED.
55391
55412
  5.6. **Mandatory gate evidence**: Before calling phase_complete, ensure:
55392
55413
  - \`.swarm/evidence/{phase}/completion-verify.json\` exists (written automatically by the completion-verify gate)
55393
55414
  - \`.swarm/evidence/{phase}/drift-verifier.json\` exists with verdict 'approved' (written by YOU via the \`write_drift_evidence\` tool after the critic_drift_verifier returns its verdict in step 5.5) \u2014 required when .swarm/spec.md exists
55394
55415
  - \`.swarm/evidence/{phase}/hallucination-guard.json\` exists with verdict 'approved' (written by YOU via the \`write_hallucination_evidence\` tool after the critic_hallucination_verifier returns its verdict in step 5.55) \u2014 ONLY required when \`hallucination_guard\` is enabled in the QA gate profile
55416
+ - \`.swarm/evidence/{phase}/mutation-gate.json\` exists with verdict 'pass' or 'warn' (written by YOU via the \`write_mutation_evidence\` tool after step 5.56) \u2014 ONLY required when \`mutation_test\` is enabled in the QA gate profile
55395
55417
  If any required file is missing, run the missing gate first. Turbo mode skips all gates automatically.
55396
- NOTE: Steps 5.5 and 5.55 are enforced by runtime hooks. If \`hallucination_guard\` is enabled and you skip the critic_hallucination_verifier delegation (or fail to call \`write_hallucination_evidence\`), phase_complete will be BLOCKED by the plugin. This is not a suggestion \u2014 it is a hard enforcement mechanism.
55418
+ NOTE: Steps 5.5, 5.55, and 5.56 are enforced by runtime hooks. If \`hallucination_guard\` is enabled and you skip the critic_hallucination_verifier delegation (or fail to call \`write_hallucination_evidence\`), phase_complete will be BLOCKED by the plugin. Similarly, if \`mutation_test\` is enabled and you skip step 5.56 (or fail to call \`write_mutation_evidence\`), phase_complete will be BLOCKED. These are not suggestions \u2014 they are hard enforcement mechanisms.
55397
55419
  6. Summarize to user
55398
55420
  7. Ask: "Ready for Phase [N+1]?"
55399
55421
 
@@ -62285,7 +62307,7 @@ var init_curator_drift = __esm(() => {
62285
62307
 
62286
62308
  // src/index.ts
62287
62309
  init_agents();
62288
- import * as path101 from "path";
62310
+ import * as path102 from "path";
62289
62311
 
62290
62312
  // src/background/index.ts
62291
62313
  init_event_bus();
@@ -76494,7 +76516,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76494
76516
  }, null, 2);
76495
76517
  }
76496
76518
  if (hasActiveTurboMode(sessionID)) {
76497
- console.warn(`[phase_complete] Turbo mode active \u2014 skipping completion-verify, drift-verifier, and hallucination-guard gates for phase ${phase}`);
76519
+ console.warn(`[phase_complete] Turbo mode active \u2014 skipping completion-verify, drift-verifier, hallucination-guard, and mutation-gate gates for phase ${phase}`);
76498
76520
  } else {
76499
76521
  try {
76500
76522
  const completionResultRaw = await executeCompletionVerify({ phase }, dir);
@@ -76671,6 +76693,78 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76671
76693
  } catch (hgError) {
76672
76694
  safeWarn(`[phase_complete] Hallucination guard error (non-blocking):`, hgError);
76673
76695
  }
76696
+ try {
76697
+ const plan = await loadPlan(dir);
76698
+ if (plan) {
76699
+ const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
76700
+ const profile = getProfile(dir, planId);
76701
+ if (profile) {
76702
+ const session2 = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
76703
+ const overrides = session2?.qaGateSessionOverrides ?? {};
76704
+ const effective = getEffectiveGates(profile, overrides);
76705
+ if (effective.mutation_test === true) {
76706
+ const mgPath = path79.join(dir, ".swarm", "evidence", String(phase), "mutation-gate.json");
76707
+ let mgVerdictFound = false;
76708
+ let mgVerdict;
76709
+ try {
76710
+ const mgContent = fs65.readFileSync(mgPath, "utf-8");
76711
+ const mgBundle = JSON.parse(mgContent);
76712
+ for (const entry of mgBundle.entries ?? []) {
76713
+ if (typeof entry.type === "string" && entry.type === "mutation-gate" && typeof entry.verdict === "string") {
76714
+ mgVerdictFound = true;
76715
+ mgVerdict = entry.verdict;
76716
+ if (entry.verdict === "fail") {
76717
+ return JSON.stringify({
76718
+ success: false,
76719
+ phase,
76720
+ status: "blocked",
76721
+ reason: "MUTATION_GATE_FAIL",
76722
+ message: `Phase ${phase} cannot be completed: mutation gate returned verdict 'fail'. Resolve surviving mutants or lower the kill-rate threshold before completing the phase.`,
76723
+ agentsDispatched,
76724
+ agentsMissing: [],
76725
+ warnings: []
76726
+ }, null, 2);
76727
+ } else if (!["pass", "warn", "skip"].includes(entry.verdict)) {
76728
+ return JSON.stringify({
76729
+ success: false,
76730
+ phase,
76731
+ status: "blocked",
76732
+ reason: "MUTATION_GATE_FAIL",
76733
+ message: `Phase ${phase} cannot be completed: mutation gate evidence contains unrecognized verdict '${entry.verdict}'. Expected one of: pass, warn, fail, skip.`,
76734
+ agentsDispatched,
76735
+ agentsMissing: [],
76736
+ warnings: []
76737
+ }, null, 2);
76738
+ }
76739
+ }
76740
+ }
76741
+ } catch (readErr) {
76742
+ if (readErr.code !== "ENOENT") {
76743
+ safeWarn(`[phase_complete] Mutation gate evidence unreadable:`, readErr);
76744
+ }
76745
+ mgVerdictFound = false;
76746
+ }
76747
+ if (!mgVerdictFound) {
76748
+ return JSON.stringify({
76749
+ success: false,
76750
+ phase,
76751
+ status: "blocked",
76752
+ reason: "MUTATION_GATE_MISSING",
76753
+ message: `Phase ${phase} cannot be completed: mutation_test is enabled and evidence not found at .swarm/evidence/${phase}/mutation-gate.json. Run mutation_test, then call write_mutation_evidence before completing the phase.`,
76754
+ agentsDispatched,
76755
+ agentsMissing: [],
76756
+ warnings: []
76757
+ }, null, 2);
76758
+ }
76759
+ if (mgVerdict === "warn") {
76760
+ safeWarn(`[phase_complete] Mutation gate verdict is 'warn' for phase ${phase} \u2014 proceeding with warning`, undefined);
76761
+ }
76762
+ }
76763
+ }
76764
+ }
76765
+ } catch (mgError) {
76766
+ safeWarn(`[phase_complete] Mutation gate error (non-blocking):`, mgError);
76767
+ }
76674
76768
  }
76675
76769
  let knowledgeConfig;
76676
76770
  try {
@@ -82123,7 +82217,7 @@ async function executeSavePlan(args2, fallbackDir) {
82123
82217
  }
82124
82218
  }
82125
82219
  var save_plan = createSwarmTool({
82126
- description: "Save a structured implementation plan to .swarm/plan.json and .swarm/plan.md. " + "Task descriptions and phase names MUST contain real content from the spec \u2014 " + "bracket placeholders like [task] or [Project] will be rejected.",
82220
+ description: "Save or revise a structured implementation plan to .swarm/plan.json and .swarm/plan.md. " + "Use this tool for all structural plan changes on an existing plan (adding/removing tasks, updating descriptions, dependencies, or phase names) \u2014 existing task statuses are preserved by default (set reset_statuses: true to start fresh). " + "Task descriptions and phase names MUST contain real content from the spec \u2014 " + "bracket placeholders like [task] or [Project] will be rejected.",
82127
82221
  args: {
82128
82222
  title: tool.schema.string().min(1).describe("Plan title \u2014 the REAL project name from the spec. NOT a placeholder like [Project]."),
82129
82223
  swarm_id: tool.schema.string().min(1).describe('Swarm identifier (e.g. "mega")'),
@@ -83960,7 +84054,8 @@ async function executeSetQaGates(args2, directory) {
83960
84054
  "sme_enabled",
83961
84055
  "critic_pre_plan",
83962
84056
  "hallucination_guard",
83963
- "sast_enabled"
84057
+ "sast_enabled",
84058
+ "mutation_test"
83964
84059
  ]) {
83965
84060
  if (args2[key] !== undefined)
83966
84061
  partial3[key] = args2[key];
@@ -84005,6 +84100,7 @@ var set_qa_gates = createSwarmTool({
84005
84100
  critic_pre_plan: tool.schema.boolean().optional().describe("Enable critic_pre_plan review before plan approval."),
84006
84101
  hallucination_guard: tool.schema.boolean().optional().describe("Enable hallucination_guard checks on plan and implementation claims."),
84007
84102
  sast_enabled: tool.schema.boolean().optional().describe("Enable SAST scanning as a required QA gate."),
84103
+ mutation_test: tool.schema.boolean().optional().describe("Enable the mutation-testing gate (default: off). Requires mutation " + "tests to achieve a passing kill rate before phase completion; " + "WARN verdict allows advancement, FAIL blocks."),
84008
84104
  project_type: tool.schema.string().optional().describe('Project type label (e.g. "ts", "python"). Only applied when the profile is being created for the first time.')
84009
84105
  },
84010
84106
  execute: async (args2, directory) => {
@@ -84337,6 +84433,161 @@ var suggestPatch = createSwarmTool({
84337
84433
  }, null, 2);
84338
84434
  }
84339
84435
  });
84436
+ // src/tools/generate-mutants.ts
84437
+ init_dist();
84438
+
84439
+ // src/mutation/generator.ts
84440
+ init_state();
84441
+ function slugify2(str) {
84442
+ return str.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_");
84443
+ }
84444
+ async function generateMutants(files, ctx) {
84445
+ if (!ctx) {
84446
+ console.warn("[generateMutants] No ToolContext \u2014 cannot call LLM; returning empty patch set");
84447
+ return [];
84448
+ }
84449
+ const client = swarmState.opencodeClient;
84450
+ if (!client) {
84451
+ console.warn("[generateMutants] opencodeClient not available; returning empty patch set");
84452
+ return [];
84453
+ }
84454
+ const directory = ctx.directory ?? process.cwd();
84455
+ let ephemeralSessionId;
84456
+ const cleanup = () => {
84457
+ if (ephemeralSessionId) {
84458
+ const id = ephemeralSessionId;
84459
+ ephemeralSessionId = undefined;
84460
+ client.session.delete({ path: { id } }).catch(() => {});
84461
+ }
84462
+ };
84463
+ try {
84464
+ const createResult = await client.session.create({
84465
+ query: { directory }
84466
+ });
84467
+ if (!createResult.data) {
84468
+ console.warn(`[generateMutants] Failed to create session: ${JSON.stringify(createResult.error)}; returning empty patch set`);
84469
+ return [];
84470
+ }
84471
+ ephemeralSessionId = createResult.data.id;
84472
+ const mutationTypes = [
84473
+ "off-by-one",
84474
+ "null-substitution",
84475
+ "operator-swap",
84476
+ "guard-removal",
84477
+ "branch-swap",
84478
+ "side-effect-deletion"
84479
+ ].join(", ");
84480
+ const promptText = `Generate mutation testing patches for the following files: ${files.join(", ")}
84481
+
84482
+ Return a JSON array where each element has:
84483
+ { id, filePath, functionName, mutationType, patch, lineNumber }
84484
+
84485
+ - id: unique string like "mut-001"
84486
+ - mutationType: one of: ${mutationTypes}
84487
+ - patch: unified diff format (--- a/file\\n+++ a/file\\n@@ ... @@\\n-old\\n+new)
84488
+ - Generate 5-10 mutations per function
84489
+
84490
+ Return ONLY valid JSON array, no markdown, no explanation.`;
84491
+ const promptResult = await client.session.prompt({
84492
+ path: { id: ephemeralSessionId },
84493
+ body: {
84494
+ agent: undefined,
84495
+ tools: { write: false, edit: false, patch: false },
84496
+ parts: [{ type: "text", text: promptText }]
84497
+ }
84498
+ });
84499
+ if (!promptResult.data) {
84500
+ console.warn(`[generateMutants] LLM prompt failed: ${JSON.stringify(promptResult.error)}; returning empty patch set`);
84501
+ return [];
84502
+ }
84503
+ const textParts = promptResult.data.parts.filter((p) => p.type === "text");
84504
+ const rawText = textParts.map((p) => p.text).join(`
84505
+ `);
84506
+ let parsed;
84507
+ try {
84508
+ parsed = JSON.parse(rawText);
84509
+ } catch (error93) {
84510
+ console.warn(`[generateMutants] Failed to parse LLM response as MutationPatch[]: ${error93 instanceof Error ? error93.message : String(error93)}; returning empty patch set`);
84511
+ return [];
84512
+ }
84513
+ if (!Array.isArray(parsed) || parsed.length === 0) {
84514
+ return [];
84515
+ }
84516
+ const patches = [];
84517
+ for (const item of parsed) {
84518
+ if (typeof item !== "object" || item === null || typeof item.filePath !== "string" || typeof item.functionName !== "string" || typeof item.mutationType !== "string" || typeof item.patch !== "string") {
84519
+ continue;
84520
+ }
84521
+ const mutationType = item.mutationType;
84522
+ const fileSlug = slugify2(item.filePath);
84523
+ const fnSlug = slugify2(item.functionName);
84524
+ const typeSlug = slugify2(mutationType);
84525
+ const idStr = typeof item.id === "string" ? item.id : "";
84526
+ const id = idStr.startsWith("mut-") ? idStr : `mut-${fileSlug}-${fnSlug}-${typeSlug}-${String(patches.length + 1).padStart(3, "0")}`;
84527
+ patches.push({
84528
+ id,
84529
+ filePath: item.filePath,
84530
+ functionName: item.functionName,
84531
+ mutationType,
84532
+ patch: item.patch,
84533
+ lineNumber: typeof item.lineNumber === "number" ? item.lineNumber : undefined
84534
+ });
84535
+ }
84536
+ return patches;
84537
+ } catch (error93) {
84538
+ console.warn(`[generateMutants] LLM call failed: ${error93 instanceof Error ? error93.message : String(error93)}; returning empty patch set`);
84539
+ return [];
84540
+ } finally {
84541
+ cleanup();
84542
+ }
84543
+ }
84544
+
84545
+ // src/tools/generate-mutants.ts
84546
+ init_create_tool();
84547
+ var generate_mutants = createSwarmTool({
84548
+ description: "Generate LLM-based mutation testing patches for the specified source files. Returns MutationPatch[] for direct consumption by the mutation_test tool. On LLM failure or when no patches can be generated, returns a SKIP verdict with a diagnostic message rather than throwing.",
84549
+ args: {
84550
+ files: tool.schema.array(tool.schema.string()).describe("Array of source file paths to generate mutation patches for")
84551
+ },
84552
+ async execute(args2, _directory, ctx) {
84553
+ const typedArgs = args2;
84554
+ if (!typedArgs.files || !Array.isArray(typedArgs.files) || typedArgs.files.length === 0) {
84555
+ const result = {
84556
+ verdict: "SKIP",
84557
+ patches: [],
84558
+ count: 0,
84559
+ message: "generate_mutants: files must be a non-empty array"
84560
+ };
84561
+ return JSON.stringify(result, null, 2);
84562
+ }
84563
+ try {
84564
+ const patches = await generateMutants(typedArgs.files, ctx);
84565
+ if (patches.length === 0) {
84566
+ const result2 = {
84567
+ verdict: "SKIP",
84568
+ patches: [],
84569
+ count: 0,
84570
+ message: "generate_mutants: LLM returned no patches \u2014 skipping mutation gate"
84571
+ };
84572
+ return JSON.stringify(result2, null, 2);
84573
+ }
84574
+ const result = {
84575
+ verdict: "ready",
84576
+ patches,
84577
+ count: patches.length
84578
+ };
84579
+ return JSON.stringify(result, null, 2);
84580
+ } catch (error93) {
84581
+ const result = {
84582
+ verdict: "SKIP",
84583
+ patches: [],
84584
+ count: 0,
84585
+ message: `generate_mutants: unexpected error \u2014 ${error93 instanceof Error ? error93.message : String(error93)}`
84586
+ };
84587
+ return JSON.stringify(result, null, 2);
84588
+ }
84589
+ }
84590
+ });
84340
84591
  // src/tools/lint-spec.ts
84341
84592
  init_spec_schema();
84342
84593
  init_create_tool();
@@ -86384,6 +86635,147 @@ var write_hallucination_evidence = createSwarmTool({
86384
86635
  }
86385
86636
  }
86386
86637
  });
86638
+ // src/tools/write-mutation-evidence.ts
86639
+ init_tool();
86640
+ init_utils2();
86641
+ init_create_tool();
86642
+ import fs85 from "fs";
86643
+ import path101 from "path";
86644
+ function normalizeVerdict3(verdict) {
86645
+ switch (verdict) {
86646
+ case "PASS":
86647
+ return "pass";
86648
+ case "WARN":
86649
+ return "warn";
86650
+ case "FAIL":
86651
+ return "fail";
86652
+ case "SKIP":
86653
+ return "skip";
86654
+ default:
86655
+ throw new Error(`Invalid verdict: must be 'PASS', 'WARN', 'FAIL', or 'SKIP', got '${verdict}'`);
86656
+ }
86657
+ }
86658
+ async function executeWriteMutationEvidence(args2, directory) {
86659
+ const phase = args2.phase;
86660
+ if (!Number.isInteger(phase) || phase < 1) {
86661
+ return JSON.stringify({
86662
+ success: false,
86663
+ phase,
86664
+ message: "Invalid phase: must be a positive integer"
86665
+ }, null, 2);
86666
+ }
86667
+ const validVerdicts = ["PASS", "WARN", "FAIL", "SKIP"];
86668
+ if (!validVerdicts.includes(args2.verdict)) {
86669
+ return JSON.stringify({
86670
+ success: false,
86671
+ phase,
86672
+ message: "Invalid verdict: must be 'PASS', 'WARN', 'FAIL', or 'SKIP'"
86673
+ }, null, 2);
86674
+ }
86675
+ if (args2.killRate !== undefined) {
86676
+ if (typeof args2.killRate !== "number" || Number.isNaN(args2.killRate)) {
86677
+ return JSON.stringify({
86678
+ success: false,
86679
+ phase,
86680
+ message: "Invalid killRate: must be a number"
86681
+ }, null, 2);
86682
+ }
86683
+ }
86684
+ if (args2.adjustedKillRate !== undefined) {
86685
+ if (typeof args2.adjustedKillRate !== "number" || Number.isNaN(args2.adjustedKillRate)) {
86686
+ return JSON.stringify({
86687
+ success: false,
86688
+ phase,
86689
+ message: "Invalid adjustedKillRate: must be a number"
86690
+ }, null, 2);
86691
+ }
86692
+ }
86693
+ const summary = args2.summary;
86694
+ if (typeof summary !== "string" || summary.trim().length === 0) {
86695
+ return JSON.stringify({
86696
+ success: false,
86697
+ phase,
86698
+ message: "Invalid summary: must be a non-empty string"
86699
+ }, null, 2);
86700
+ }
86701
+ const normalizedVerdict = normalizeVerdict3(args2.verdict);
86702
+ const evidenceEntry = {
86703
+ type: "mutation-gate",
86704
+ verdict: normalizedVerdict,
86705
+ killRate: args2.killRate ?? 0,
86706
+ adjustedKillRate: args2.adjustedKillRate ?? 0,
86707
+ summary: summary.trim(),
86708
+ timestamp: new Date().toISOString()
86709
+ };
86710
+ if (args2.survivedMutants !== undefined) {
86711
+ evidenceEntry.survivedMutants = args2.survivedMutants;
86712
+ }
86713
+ const evidenceContent = {
86714
+ entries: [evidenceEntry]
86715
+ };
86716
+ const filename = "mutation-gate.json";
86717
+ const relativePath = path101.join("evidence", String(phase), filename);
86718
+ let validatedPath;
86719
+ try {
86720
+ validatedPath = validateSwarmPath(directory, relativePath);
86721
+ } catch (error93) {
86722
+ return JSON.stringify({
86723
+ success: false,
86724
+ phase,
86725
+ message: error93 instanceof Error ? error93.message : "Failed to validate path"
86726
+ }, null, 2);
86727
+ }
86728
+ const evidenceDir = path101.dirname(validatedPath);
86729
+ try {
86730
+ await fs85.promises.mkdir(evidenceDir, { recursive: true });
86731
+ const tempPath = path101.join(evidenceDir, `.${filename}.tmp`);
86732
+ await fs85.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
86733
+ await fs85.promises.rename(tempPath, validatedPath);
86734
+ return JSON.stringify({
86735
+ success: true,
86736
+ phase,
86737
+ verdict: normalizedVerdict,
86738
+ message: `Mutation gate evidence written to .swarm/evidence/${phase}/mutation-gate.json`
86739
+ }, null, 2);
86740
+ } catch (error93) {
86741
+ return JSON.stringify({
86742
+ success: false,
86743
+ phase,
86744
+ message: error93 instanceof Error ? error93.message : String(error93)
86745
+ }, null, 2);
86746
+ }
86747
+ }
86748
+ var write_mutation_evidence = createSwarmTool({
86749
+ description: 'Write mutation gate evidence for a completed phase. Accepts phase, verdict (PASS/WARN/FAIL/SKIP), killRate, adjustedKillRate, summary, and optional survivedMutants. Normalizes uppercase verdicts to lowercase (PASS\u2192pass, WARN\u2192warn, FAIL\u2192fail, SKIP\u2192skip) and writes entries[0].type="mutation-gate" to .swarm/evidence/{phase}/mutation-gate.json using atomic temp+rename write. Use this after mutation_test tool returns to persist the gate verdict.',
86750
+ args: {
86751
+ phase: tool.schema.number().int().min(1).describe("The phase number for the mutation gate (e.g., 1, 2, 3)"),
86752
+ verdict: tool.schema.enum(["PASS", "WARN", "FAIL", "SKIP"]).describe("Verdict of the mutation gate: 'PASS', 'WARN', 'FAIL', or 'SKIP'"),
86753
+ killRate: tool.schema.number().optional().describe("The raw kill rate (e.g., 0.85)"),
86754
+ adjustedKillRate: tool.schema.number().optional().describe("The adjusted kill rate accounting for timeout survived mutants (e.g., 0.87)"),
86755
+ summary: tool.schema.string().describe("Human-readable summary of the mutation gate result"),
86756
+ survivedMutants: tool.schema.string().optional().describe("Optional JSON-serialized list of survived mutants")
86757
+ },
86758
+ execute: async (args2, directory) => {
86759
+ const rawPhase = args2.phase !== undefined ? Number(args2.phase) : 0;
86760
+ try {
86761
+ const typedArgs = {
86762
+ phase: Number(args2.phase),
86763
+ verdict: String(args2.verdict),
86764
+ killRate: args2.killRate !== undefined ? Number(args2.killRate) : undefined,
86765
+ adjustedKillRate: args2.adjustedKillRate !== undefined ? Number(args2.adjustedKillRate) : undefined,
86766
+ summary: String(args2.summary ?? ""),
86767
+ survivedMutants: args2.survivedMutants !== undefined ? String(args2.survivedMutants) : undefined
86768
+ };
86769
+ return await executeWriteMutationEvidence(typedArgs, directory);
86770
+ } catch (error93) {
86771
+ return JSON.stringify({
86772
+ success: false,
86773
+ phase: rawPhase,
86774
+ message: error93 instanceof Error ? error93.message : "Unknown error"
86775
+ }, null, 2);
86776
+ }
86777
+ }
86778
+ });
86387
86779
 
86388
86780
  // src/tools/index.ts
86389
86781
  init_write_retro();
@@ -86556,7 +86948,7 @@ var OpenCodeSwarm = async (ctx) => {
86556
86948
  const { PreflightTriggerManager: PTM } = await Promise.resolve().then(() => (init_trigger(), exports_trigger));
86557
86949
  preflightTriggerManager = new PTM(automationConfig);
86558
86950
  const { AutomationStatusArtifact: ASA } = await Promise.resolve().then(() => (init_status_artifact(), exports_status_artifact));
86559
- const swarmDir = path101.resolve(ctx.directory, ".swarm");
86951
+ const swarmDir = path102.resolve(ctx.directory, ".swarm");
86560
86952
  statusArtifact = new ASA(swarmDir);
86561
86953
  statusArtifact.updateConfig(automationConfig.mode, automationConfig.capabilities);
86562
86954
  if (automationConfig.capabilities?.evidence_auto_summaries === true) {
@@ -86670,6 +87062,7 @@ var OpenCodeSwarm = async (ctx) => {
86670
87062
  co_change_analyzer,
86671
87063
  detect_domains,
86672
87064
  mutation_test,
87065
+ generate_mutants,
86673
87066
  doc_extract,
86674
87067
  doc_scan,
86675
87068
  evidence_check,
@@ -86710,6 +87103,7 @@ var OpenCodeSwarm = async (ctx) => {
86710
87103
  write_retro,
86711
87104
  write_drift_evidence,
86712
87105
  write_hallucination_evidence,
87106
+ write_mutation_evidence,
86713
87107
  declare_scope
86714
87108
  },
86715
87109
  config: async (opencodeConfig) => {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * LLM-based mutation patch generator.
3
+ *
4
+ * Uses the opencode SDK to call an LLM session and generate mutation testing patches
5
+ * for specified source files. Produces MutationPatch[] for use by executeMutationSuite.
6
+ */
7
+ import type { ToolContext } from '@opencode-ai/plugin';
8
+ import type { MutationPatch } from './engine.js';
9
+ /**
10
+ * Generate mutation testing patches for the given source files using an LLM.
11
+ *
12
+ * @param files - Array of file paths to generate mutations for
13
+ * @param ctx - Optional ToolContext providing sessionID and directory
14
+ * @returns Promise<MutationPatch[]> array of mutation patches, never throws
15
+ */
16
+ export declare function generateMutants(files: string[], ctx?: ToolContext): Promise<MutationPatch[]>;
@@ -16,11 +16,11 @@ export declare const ArgsSchema: z.ZodObject<{
16
16
  roundNumber: z.ZodDefault<z.ZodNumber>;
17
17
  verdicts: z.ZodArray<z.ZodObject<{
18
18
  agent: z.ZodEnum<{
19
- reviewer: "reviewer";
20
- test_engineer: "test_engineer";
21
- explorer: "explorer";
22
19
  sme: "sme";
20
+ reviewer: "reviewer";
23
21
  critic: "critic";
22
+ explorer: "explorer";
23
+ test_engineer: "test_engineer";
24
24
  }>;
25
25
  verdict: z.ZodEnum<{
26
26
  APPROVE: "APPROVE";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Generate mutation patches tool.
3
+ * Calls generateMutants() from src/mutation/generator.ts with the ToolContext
4
+ * and returns the patch list for piping into the mutation_test tool.
5
+ * On LLM failure, emits a SKIP verdict with a diagnostic message.
6
+ */
7
+ import type { MutationPatch } from '../mutation/engine.js';
8
+ import { createSwarmTool } from './create-tool';
9
+ export interface GenerateMutantsResult {
10
+ verdict: 'ready' | 'SKIP';
11
+ patches: MutationPatch[];
12
+ count: number;
13
+ message?: string;
14
+ }
15
+ export declare const generate_mutants: ReturnType<typeof createSwarmTool>;