opencode-swarm 6.81.0 → 6.82.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 +187 -701
- package/dist/adversarial-tests.test.d.ts +1 -0
- package/dist/agents/architect.d.ts +1 -1
- package/dist/cli/index.js +23 -9
- package/dist/commands/registry.d.ts +1 -1
- package/dist/config/schema.d.ts +4 -4
- package/dist/db/qa-gate-profile.d.ts +4 -1
- package/dist/index.js +405 -11
- package/dist/mutation/__tests__/generator.test.d.ts +1 -0
- package/dist/mutation/generator.d.ts +16 -0
- package/dist/tools/convene-council.d.ts +3 -3
- package/dist/tools/generate-mutants.d.ts +15 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/set-qa-gates.d.ts +1 -0
- package/dist/tools/tool-names.d.ts +1 -1
- package/dist/tools/write-mutation-evidence.d.ts +34 -0
- package/dist/tools/write-mutation-evidence.test.d.ts +1 -0
- package/package.json +1 -1
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
|
|
|
@@ -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
|
|
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
|
}
|
|
@@ -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.
|
|
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
|
|
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,
|
|
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 {
|
|
@@ -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 =
|
|
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>;
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export type { ClassifiedFailure, FailureClassification, FailureCluster, } from '
|
|
|
49
49
|
export { classifyAndCluster, classifyFailure, clusterFailures, } from '../test-impact/failure-classifier.js';
|
|
50
50
|
export type { FlakyTestEntry } from '../test-impact/flaky-detector.js';
|
|
51
51
|
export { computeFlakyScore, detectFlakyTests, isTestQuarantined, } from '../test-impact/flaky-detector.js';
|
|
52
|
+
export { generate_mutants } from './generate-mutants';
|
|
52
53
|
export { lint_spec } from './lint-spec';
|
|
53
54
|
export { mutation_test } from './mutation-test';
|
|
54
55
|
export { symbols } from './symbols';
|
|
@@ -59,4 +60,5 @@ export { todo_extract } from './todo-extract';
|
|
|
59
60
|
export { executeUpdateTaskStatus, type UpdateTaskStatusArgs, type UpdateTaskStatusResult, update_task_status, } from './update-task-status';
|
|
60
61
|
export { write_drift_evidence } from './write-drift-evidence';
|
|
61
62
|
export { write_hallucination_evidence } from './write-hallucination-evidence';
|
|
63
|
+
export { write_mutation_evidence } from './write-mutation-evidence';
|
|
62
64
|
export { executeWriteRetro, write_retro } from './write-retro';
|