opencode-swarm 6.40.4 → 6.40.5
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 +116 -5
- package/dist/tools/pre-check-batch.d.ts +26 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -42001,8 +42001,9 @@ All other gates: failure \u2192 return to coder. No self-fixes. No workarounds.
|
|
|
42001
42001
|
- quality_budget (maintainability metrics)
|
|
42002
42002
|
\u2192 Returns { gates_passed, lint, secretscan, sast_scan, quality_budget, total_duration_ms }
|
|
42003
42003
|
\u2192 If gates_passed === false: read individual tool results, identify which tool(s) failed, return structured rejection to {{AGENT_PREFIX}}coder with specific tool failures. Do NOT call {{AGENT_PREFIX}}reviewer.
|
|
42004
|
-
\u2192 If gates_passed === true: proceed to {{AGENT_PREFIX}}reviewer.
|
|
42005
|
-
\u2192
|
|
42004
|
+
\u2192 If gates_passed === true AND sast_preexisting_findings is present: proceed to {{AGENT_PREFIX}}reviewer. Include the pre-existing SAST findings in the reviewer delegation context with instruction: "SAST TRIAGE REQUIRED: The following HIGH/CRITICAL SAST findings exist on unchanged lines in this changeset. Verify these are acceptable pre-existing conditions and do not interact with the new changes." Do NOT return to coder for pre-existing findings on unchanged code.
|
|
42005
|
+
\u2192 If gates_passed === true (no sast_preexisting_findings): proceed to {{AGENT_PREFIX}}reviewer.
|
|
42006
|
+
\u2192 REQUIRED: Print "pre_check_batch: [PASS \u2014 all gates passed | PASS \u2014 pre-existing SAST findings on unchanged lines (N findings, reviewer triage) | FAIL \u2014 [gate]: [details]]"
|
|
42006
42007
|
|
|
42007
42008
|
\u26A0\uFE0F pre_check_batch SCOPE BOUNDARY:
|
|
42008
42009
|
pre_check_batch runs FOUR automated tools: lint:check, secretscan, sast_scan, quality_budget.
|
|
@@ -64125,6 +64126,99 @@ async function runQualityBudgetWrapped(changedFiles, directory, _config) {
|
|
|
64125
64126
|
};
|
|
64126
64127
|
}
|
|
64127
64128
|
}
|
|
64129
|
+
var GATE_SEVERITIES = new Set(["high", "critical"]);
|
|
64130
|
+
async function runGitDiff(args2, directory) {
|
|
64131
|
+
try {
|
|
64132
|
+
const proc = Bun.spawn(["git", "diff", ...args2], {
|
|
64133
|
+
cwd: directory,
|
|
64134
|
+
stdout: "pipe",
|
|
64135
|
+
stderr: "pipe"
|
|
64136
|
+
});
|
|
64137
|
+
const [exitCode, stdout] = await Promise.all([
|
|
64138
|
+
proc.exited,
|
|
64139
|
+
new Response(proc.stdout).text()
|
|
64140
|
+
]);
|
|
64141
|
+
if (exitCode !== 0)
|
|
64142
|
+
return null;
|
|
64143
|
+
const trimmed = stdout.trim();
|
|
64144
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
64145
|
+
} catch {
|
|
64146
|
+
return null;
|
|
64147
|
+
}
|
|
64148
|
+
}
|
|
64149
|
+
function parseDiffLineRanges(diffOutput) {
|
|
64150
|
+
const result = new Map;
|
|
64151
|
+
let currentFile = null;
|
|
64152
|
+
for (const line of diffOutput.split(`
|
|
64153
|
+
`)) {
|
|
64154
|
+
if (line.startsWith("+++ b/")) {
|
|
64155
|
+
currentFile = line.slice(6).trim();
|
|
64156
|
+
if (!result.has(currentFile)) {
|
|
64157
|
+
result.set(currentFile, new Set);
|
|
64158
|
+
}
|
|
64159
|
+
continue;
|
|
64160
|
+
}
|
|
64161
|
+
if (line.startsWith("@@") && currentFile) {
|
|
64162
|
+
const match = line.match(/^@@ [^+]*\+(\d+)(?:,(\d+))? @@/);
|
|
64163
|
+
if (match) {
|
|
64164
|
+
const start2 = parseInt(match[1], 10);
|
|
64165
|
+
const count = match[2] !== undefined ? parseInt(match[2], 10) : 1;
|
|
64166
|
+
const lines = result.get(currentFile);
|
|
64167
|
+
for (let i2 = start2;i2 < start2 + count; i2++) {
|
|
64168
|
+
lines.add(i2);
|
|
64169
|
+
}
|
|
64170
|
+
}
|
|
64171
|
+
}
|
|
64172
|
+
}
|
|
64173
|
+
return result;
|
|
64174
|
+
}
|
|
64175
|
+
async function getChangedLineRanges(directory) {
|
|
64176
|
+
try {
|
|
64177
|
+
for (const baseBranch of ["origin/main", "origin/master", "main", "master"]) {
|
|
64178
|
+
const mergeBaseProc = Bun.spawn(["git", "merge-base", baseBranch, "HEAD"], { cwd: directory, stdout: "pipe", stderr: "pipe" });
|
|
64179
|
+
const [mbExit, mbOut] = await Promise.all([
|
|
64180
|
+
mergeBaseProc.exited,
|
|
64181
|
+
new Response(mergeBaseProc.stdout).text()
|
|
64182
|
+
]);
|
|
64183
|
+
if (mbExit === 0 && mbOut.trim()) {
|
|
64184
|
+
const mergeBase = mbOut.trim();
|
|
64185
|
+
const diffOut = await runGitDiff(["-U0", `${mergeBase}..HEAD`], directory);
|
|
64186
|
+
if (diffOut) {
|
|
64187
|
+
return parseDiffLineRanges(diffOut);
|
|
64188
|
+
}
|
|
64189
|
+
}
|
|
64190
|
+
}
|
|
64191
|
+
const diffHead1 = await runGitDiff(["-U0", "HEAD~1"], directory);
|
|
64192
|
+
if (diffHead1) {
|
|
64193
|
+
return parseDiffLineRanges(diffHead1);
|
|
64194
|
+
}
|
|
64195
|
+
const diffHead = await runGitDiff(["-U0", "HEAD"], directory);
|
|
64196
|
+
if (diffHead) {
|
|
64197
|
+
return parseDiffLineRanges(diffHead);
|
|
64198
|
+
}
|
|
64199
|
+
return null;
|
|
64200
|
+
} catch {
|
|
64201
|
+
return null;
|
|
64202
|
+
}
|
|
64203
|
+
}
|
|
64204
|
+
function classifySastFindings(findings, changedLineRanges, directory) {
|
|
64205
|
+
if (!changedLineRanges || changedLineRanges.size === 0) {
|
|
64206
|
+
return { newFindings: findings, preexistingFindings: [] };
|
|
64207
|
+
}
|
|
64208
|
+
const newFindings = [];
|
|
64209
|
+
const preexistingFindings = [];
|
|
64210
|
+
for (const finding of findings) {
|
|
64211
|
+
const filePath = finding.location.file;
|
|
64212
|
+
const normalised = path53.relative(directory, filePath).replace(/\\/g, "/");
|
|
64213
|
+
const changedLines = changedLineRanges.get(normalised);
|
|
64214
|
+
if (changedLines && changedLines.has(finding.location.line)) {
|
|
64215
|
+
newFindings.push(finding);
|
|
64216
|
+
} else {
|
|
64217
|
+
preexistingFindings.push(finding);
|
|
64218
|
+
}
|
|
64219
|
+
}
|
|
64220
|
+
return { newFindings, preexistingFindings };
|
|
64221
|
+
}
|
|
64128
64222
|
async function runPreCheckBatch(input, workspaceDir, contextDir) {
|
|
64129
64223
|
const effectiveWorkspaceDir = workspaceDir || input.directory || contextDir;
|
|
64130
64224
|
const { files, directory, sast_threshold = "medium", config: config3 } = input;
|
|
@@ -64233,10 +64327,24 @@ async function runPreCheckBatch(input, workspaceDir, contextDir) {
|
|
|
64233
64327
|
warn(`Failed to persist secretscan evidence: ${e instanceof Error ? e.message : String(e)}`);
|
|
64234
64328
|
}
|
|
64235
64329
|
}
|
|
64330
|
+
let sastPreexistingFindings;
|
|
64236
64331
|
if (sastScanResult.ran && sastScanResult.result) {
|
|
64237
64332
|
if (sastScanResult.result.verdict === "fail") {
|
|
64238
|
-
|
|
64239
|
-
|
|
64333
|
+
const gateFindings = sastScanResult.result.findings.filter((f) => GATE_SEVERITIES.has(f.severity));
|
|
64334
|
+
if (gateFindings.length > 0) {
|
|
64335
|
+
const changedLineRanges = await getChangedLineRanges(directory);
|
|
64336
|
+
const { newFindings, preexistingFindings } = classifySastFindings(gateFindings, changedLineRanges, directory);
|
|
64337
|
+
if (newFindings.length > 0) {
|
|
64338
|
+
gatesPassed = false;
|
|
64339
|
+
warn(`pre_check_batch: SAST scan found ${newFindings.length} new HIGH/CRITICAL finding(s) on changed lines - GATE FAILED`);
|
|
64340
|
+
} else if (preexistingFindings.length > 0) {
|
|
64341
|
+
sastPreexistingFindings = preexistingFindings;
|
|
64342
|
+
warn(`pre_check_batch: SAST scan found ${preexistingFindings.length} pre-existing HIGH/CRITICAL finding(s) on unchanged lines - passing to reviewer for triage`);
|
|
64343
|
+
}
|
|
64344
|
+
} else {
|
|
64345
|
+
gatesPassed = false;
|
|
64346
|
+
warn("pre_check_batch: SAST scan found vulnerabilities - GATE FAILED");
|
|
64347
|
+
}
|
|
64240
64348
|
}
|
|
64241
64349
|
} else if (sastScanResult.error) {
|
|
64242
64350
|
gatesPassed = false;
|
|
@@ -64255,7 +64363,10 @@ async function runPreCheckBatch(input, workspaceDir, contextDir) {
|
|
|
64255
64363
|
secretscan: secretscanResult,
|
|
64256
64364
|
sast_scan: sastScanResult,
|
|
64257
64365
|
quality_budget: qualityBudgetResult,
|
|
64258
|
-
total_duration_ms: Math.round(totalDuration)
|
|
64366
|
+
total_duration_ms: Math.round(totalDuration),
|
|
64367
|
+
...sastPreexistingFindings && sastPreexistingFindings.length > 0 && {
|
|
64368
|
+
sast_preexisting_findings: sastPreexistingFindings
|
|
64369
|
+
}
|
|
64259
64370
|
};
|
|
64260
64371
|
const outputSize = JSON.stringify(result).length;
|
|
64261
64372
|
if (outputSize > MAX_COMBINED_BYTES) {
|
|
@@ -7,7 +7,7 @@ import { tool } from '@opencode-ai/plugin';
|
|
|
7
7
|
import type { PluginConfig } from '../config';
|
|
8
8
|
import type { LintResult } from './lint';
|
|
9
9
|
import type { QualityBudgetResult } from './quality-budget';
|
|
10
|
-
import type { SastScanResult } from './sast-scan';
|
|
10
|
+
import type { SastScanFinding, SastScanResult } from './sast-scan';
|
|
11
11
|
import type { SecretscanErrorResult, SecretscanResult } from './secretscan';
|
|
12
12
|
export interface PreCheckBatchInput {
|
|
13
13
|
/** List of specific files to check (optional) */
|
|
@@ -42,7 +42,32 @@ export interface PreCheckBatchResult {
|
|
|
42
42
|
quality_budget: ToolResult<QualityBudgetResult>;
|
|
43
43
|
/** Total duration in milliseconds */
|
|
44
44
|
total_duration_ms: number;
|
|
45
|
+
/** Pre-existing SAST findings on unchanged lines, requiring reviewer triage */
|
|
46
|
+
sast_preexisting_findings?: SastScanFinding[];
|
|
45
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse unified diff output (with -U0) to extract added/modified line numbers per file.
|
|
50
|
+
* Returns a Map from normalised file path → Set of changed line numbers.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseDiffLineRanges(diffOutput: string): Map<string, Set<number>>;
|
|
53
|
+
/**
|
|
54
|
+
* Get changed line ranges for the current branch vs its base.
|
|
55
|
+
* Tries three strategies in order:
|
|
56
|
+
* 1. merge-base diff against main/master (captures all branch changes, works after commit)
|
|
57
|
+
* 2. HEAD~1 (single-commit diff, works after commit)
|
|
58
|
+
* 3. HEAD (unstaged/staged changes, works before commit)
|
|
59
|
+
* Returns null if git is unavailable or no changes found.
|
|
60
|
+
*/
|
|
61
|
+
export declare function getChangedLineRanges(directory: string): Promise<Map<string, Set<number>> | null>;
|
|
62
|
+
/**
|
|
63
|
+
* Classify SAST findings as "new" (on changed lines) or "pre-existing" (unchanged lines).
|
|
64
|
+
* A finding is "new" if its file+line intersects the changed line ranges from git diff.
|
|
65
|
+
* If line ranges cannot be determined (git unavailable), all findings are treated as new (fail-closed).
|
|
66
|
+
*/
|
|
67
|
+
export declare function classifySastFindings(findings: SastScanFinding[], changedLineRanges: Map<string, Set<number>> | null, directory: string): {
|
|
68
|
+
newFindings: SastScanFinding[];
|
|
69
|
+
preexistingFindings: SastScanFinding[];
|
|
70
|
+
};
|
|
46
71
|
/**
|
|
47
72
|
* Run all 4 pre-check tools in parallel with concurrency limit
|
|
48
73
|
* @param input - The pre-check batch input
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm",
|
|
3
|
-
"version": "6.40.
|
|
3
|
+
"version": "6.40.5",
|
|
4
4
|
"description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|