opencode-swarm 6.40.3 → 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 CHANGED
@@ -41191,7 +41191,11 @@ function applyRehydrationCache(session) {
41191
41191
  const evidence = evidenceMap.get(taskId);
41192
41192
  if (evidence) {
41193
41193
  const derivedState = evidenceToWorkflowState(evidence);
41194
- session.taskWorkflowStates.set(taskId, derivedState);
41194
+ const existingIndex = existingState ? STATE_ORDER.indexOf(existingState) : -1;
41195
+ const derivedIndex = STATE_ORDER.indexOf(derivedState);
41196
+ if (derivedIndex > existingIndex) {
41197
+ session.taskWorkflowStates.set(taskId, derivedState);
41198
+ }
41195
41199
  } else {
41196
41200
  const existingIndex = existingState ? STATE_ORDER.indexOf(existingState) : -1;
41197
41201
  const derivedIndex = STATE_ORDER.indexOf(planState);
@@ -41997,8 +42001,9 @@ All other gates: failure \u2192 return to coder. No self-fixes. No workarounds.
41997
42001
  - quality_budget (maintainability metrics)
41998
42002
  \u2192 Returns { gates_passed, lint, secretscan, sast_scan, quality_budget, total_duration_ms }
41999
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.
42000
- \u2192 If gates_passed === true: proceed to {{AGENT_PREFIX}}reviewer.
42001
- \u2192 REQUIRED: Print "pre_check_batch: [PASS \u2014 all gates passed | FAIL \u2014 [gate]: [details]]"
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]]"
42002
42007
 
42003
42008
  \u26A0\uFE0F pre_check_batch SCOPE BOUNDARY:
42004
42009
  pre_check_batch runs FOUR automated tools: lint:check, secretscan, sast_scan, quality_budget.
@@ -64121,6 +64126,99 @@ async function runQualityBudgetWrapped(changedFiles, directory, _config) {
64121
64126
  };
64122
64127
  }
64123
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
+ }
64124
64222
  async function runPreCheckBatch(input, workspaceDir, contextDir) {
64125
64223
  const effectiveWorkspaceDir = workspaceDir || input.directory || contextDir;
64126
64224
  const { files, directory, sast_threshold = "medium", config: config3 } = input;
@@ -64229,10 +64327,24 @@ async function runPreCheckBatch(input, workspaceDir, contextDir) {
64229
64327
  warn(`Failed to persist secretscan evidence: ${e instanceof Error ? e.message : String(e)}`);
64230
64328
  }
64231
64329
  }
64330
+ let sastPreexistingFindings;
64232
64331
  if (sastScanResult.ran && sastScanResult.result) {
64233
64332
  if (sastScanResult.result.verdict === "fail") {
64234
- gatesPassed = false;
64235
- warn("pre_check_batch: SAST scan found vulnerabilities - GATE FAILED");
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
+ }
64236
64348
  }
64237
64349
  } else if (sastScanResult.error) {
64238
64350
  gatesPassed = false;
@@ -64251,7 +64363,10 @@ async function runPreCheckBatch(input, workspaceDir, contextDir) {
64251
64363
  secretscan: secretscanResult,
64252
64364
  sast_scan: sastScanResult,
64253
64365
  quality_budget: qualityBudgetResult,
64254
- total_duration_ms: Math.round(totalDuration)
64366
+ total_duration_ms: Math.round(totalDuration),
64367
+ ...sastPreexistingFindings && sastPreexistingFindings.length > 0 && {
64368
+ sast_preexisting_findings: sastPreexistingFindings
64369
+ }
64255
64370
  };
64256
64371
  const outputSize = JSON.stringify(result).length;
64257
64372
  if (outputSize > MAX_COMBINED_BYTES) {
package/dist/state.d.ts CHANGED
@@ -319,7 +319,7 @@ export declare function buildRehydrationCache(directory: string): Promise<void>;
319
319
  /**
320
320
  * Synchronously applies the cached plan+evidence data to a session.
321
321
  * Merge rules:
322
- * - evidence-derived state: always applied (replaces snapshot state, even if lower)
322
+ * - evidence-derived state: only applied if it advances past existing state
323
323
  * - plan-only derived state: only applied if it advances past existing state
324
324
  * No-op when the cache has not been built yet.
325
325
  */
@@ -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",
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",