opencode-swarm 5.2.0 β†’ 6.0.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/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <p align="center">
2
- <img src="https://img.shields.io/badge/version-5.2.0-blue" alt="Version">
2
+ <img src="https://img.shields.io/badge/version-6.0.0-blue" alt="Version">
3
3
  <img src="https://img.shields.io/badge/license-MIT-green" alt="License">
4
4
  <img src="https://img.shields.io/badge/opencode-plugin-purple" alt="OpenCode Plugin">
5
5
  <img src="https://img.shields.io/badge/agents-7-orange" alt="Agents">
6
- <img src="https://img.shields.io/badge/tests-1101-brightgreen" alt="Tests">
6
+ <img src="https://img.shields.io/badge/tests-1188-brightgreen" alt="Tests">
7
7
  </p>
8
8
 
9
9
  <h1 align="center">🐝 OpenCode Swarm</h1>
@@ -138,15 +138,24 @@ OpenCode Swarm:
138
138
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
139
139
  β”‚ PHASE 5: Execute (per task) β”‚
140
140
  β”‚ β”‚
141
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
142
- β”‚ β”‚ @coder β”‚ β†’ β”‚ @reviewer β”‚ β†’ β”‚ @test β”‚ β”‚
143
- β”‚ β”‚ 1 task β”‚ β”‚ check all β”‚ β”‚ write + run β”‚ β”‚
144
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
145
- β”‚ β”‚ β”‚ β”‚ β”‚
146
- β”‚ β”‚ If REJECTED: retry If FAIL: fix + retest β”‚
147
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
141
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
142
+ β”‚ β”‚ @coder β”‚ β†’ β”‚ diff β”‚ β†’ β”‚ @reviewer β”‚ β†’ β”‚ @test β”‚ β”‚
143
+ β”‚ β”‚ 1 task β”‚ β”‚ tool β”‚ β”‚ check all β”‚ β”‚ write + run β”‚ β”‚
144
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
145
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
146
+ β”‚ β”‚ Contract β”‚ If REJECTED: If FAIL: fix β”‚
147
+ β”‚ β”‚ changes? β”‚ retry from coder + retest β”‚
148
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
149
+ β”‚ β”‚ β–Ό β”‚ β–Ό β”‚
150
+ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
151
+ β”‚ β”‚ β”‚@explorerβ”‚ β”‚ β”‚ @reviewer β”‚ β†’ β”‚ @test β”‚ β”‚
152
+ β”‚ β”‚ β”‚ impact β”‚ β”‚ β”‚ security-onlyβ”‚ β”‚ adversarial β”‚ β”‚
153
+ β”‚ β”‚ β”‚analysis β”‚ β”‚ β”‚ (if match) β”‚ β”‚ (attacks) β”‚ β”‚
154
+ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
155
+ β”‚ β”‚ β”‚ β”‚
156
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
148
157
  β”‚ β”‚
149
- β”‚ Update plan.md: [x] Task complete (only if PASS) β”‚
158
+ β”‚ Update plan.md: [x] Task complete (only after ALL gates pass) β”‚
150
159
  β”‚ Next task... β”‚
151
160
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
152
161
  β”‚
@@ -334,6 +343,13 @@ bunx opencode-swarm uninstall --clean
334
343
 
335
344
  ## What's New
336
345
 
346
+ ### v6.0.0 β€” Core QA & Security Gates
347
+ - **Dual-pass security reviewer** β€” After the general reviewer APPROVES, the architect automatically triggers a second security-only review pass when the changed file matches security-sensitive paths (`auth`, `crypto`, `session`, `token`, `middleware`, `api`, `security`) or the coder's output contains security keywords. Configurable via `review_passes` config.
348
+ - **Adversarial testing** β€” After verification tests PASS, the test engineer is re-delegated with adversarial-only framing: attack vectors, boundary violations, and injection attempts. Pure prompt engineering, no new infrastructure.
349
+ - **Integration impact analysis** β€” After the coder completes, the `diff` tool detects contract changes (exported functions, interfaces, types). If found, the explorer runs impact analysis across dependents before review begins.
350
+ - **`diff` tool** β€” New agent-accessible tool providing structured git diff with numstat parsing, contract change detection, configurable base ref (`HEAD`/staged/unstaged), path filtering, and 500-line truncation.
351
+ - **87 new tests** β€” 1188 total tests across 53+ files (up from 1101 in v5.2.0).
352
+
337
353
  ### v5.2.0 β€” Per-Invocation Guardrails
338
354
  - **Per-invocation budget isolation** β€” Guardrail limits (tool calls, duration, errors) now reset with each agent delegation. Second invocation of the same agent gets a fresh budget, preventing false circuit breaker trips in long-running projects.
339
355
  - **Architect protocol enforcement** β€” New mandatory QA gate rules: every coder task must go through reviewer approval + test_engineer verification before the next coder task. Protocol violations detected at runtime with warning injection.
@@ -409,7 +425,7 @@ All features are opt-in via configuration. See [Installation Guide](docs/install
409
425
  ### βœ… Quality Assurance
410
426
  | Agent | Role |
411
427
  |-------|------|
412
- | `reviewer` | Combined correctness + security review. The architect specifies CHECK dimensions (security, correctness, edge-cases, performance, etc.) per call. |
428
+ | `reviewer` | Dual-pass review: correctness review first, then automatic security-only pass for security-sensitive files. The architect specifies CHECK dimensions per call. OWASP Top 10 categories built in. |
413
429
  | `critic` | Plan review gate. Reviews the architect's plan BEFORE implementation β€” checks completeness, feasibility, scope, dependencies, and flags AI-slop. |
414
430
 
415
431
  ---
@@ -516,6 +532,38 @@ Override limits for specific agents that need more (or less) room:
516
532
 
517
533
  Profiles merge with base config β€” only specified fields are overridden.
518
534
 
535
+ ### Review Passes
536
+
537
+ Control the dual-pass security review behavior:
538
+
539
+ ```jsonc
540
+ {
541
+ "review_passes": {
542
+ "always_security_review": false, // default: false (only on security-sensitive files)
543
+ "security_globs": [ // default patterns:
544
+ "**/*auth*", "**/*crypto*",
545
+ "**/*session*", "**/*token*",
546
+ "**/*middleware*", "**/*api*",
547
+ "**/*security*"
548
+ ]
549
+ }
550
+ }
551
+ ```
552
+
553
+ Set `always_security_review: true` to run the security pass on every task, regardless of file path.
554
+
555
+ ### Integration Analysis
556
+
557
+ Control whether contract change detection triggers impact analysis:
558
+
559
+ ```jsonc
560
+ {
561
+ "integration_analysis": {
562
+ "enabled": true // default: true
563
+ }
564
+ }
565
+ ```
566
+
519
567
  > **Architect is exempt/unlimited by default:** The architect agent has no guardrail limits by default. To override, add a `profiles.architect` entry in your guardrails config.
520
568
 
521
569
  ### Per-Invocation Budgets
@@ -549,7 +597,7 @@ This prevents long-running projects from accumulating session-wide counters that
549
597
  | Execution | Serial (predictable) | Parallel (chaotic) | Parallel | Configurable |
550
598
  | Planning | Phased with acceptance criteria | Ad-hoc | Role-based | Graph-based |
551
599
  | Memory | Persistent `.swarm/` files | Session only | Session only | Checkpoints |
552
- | QA | Per-task (unified review) | Optional | Optional | Manual |
600
+ | QA | Dual-pass per-task (review + security + adversarial) | Optional | Optional | Manual |
553
601
  | Model mixing | Per-agent configuration | Limited | Limited | Manual |
554
602
  | Resume projects | βœ… Native | ❌ | ❌ | Partial |
555
603
  | SME domains | Open-domain (any) | Generic | Generic | Generic |
@@ -561,7 +609,7 @@ This prevents long-running projects from accumulating session-wide counters that
561
609
 
562
610
  1. **Plan before code** - Documented phases with acceptance criteria
563
611
  2. **One task at a time** - Focused work, quality output
564
- 3. **Review everything immediately** - Correctness + security review per task, not per project
612
+ 3. **Review everything immediately** - Dual-pass review (correctness + security) with adversarial testing per task
565
613
  4. **Cache SME knowledge** - Don't re-ask answered questions
566
614
  5. **Persistent memory** - `.swarm/` files survive sessions
567
615
  6. **Serial execution** - Predictable, debuggable, no race conditions
@@ -582,7 +630,7 @@ bun test
582
630
  bun test tests/unit/config/schema.test.ts
583
631
  ```
584
632
 
585
- 1101 tests across 48 files covering config, tools, agents, hooks, commands, state, guardrails, evidence, plan schemas, circuit breaker race conditions, invocation windows, and multi-invocation isolation. Uses Bun's built-in test runner β€” zero additional test dependencies.
633
+ 1188 tests across 53+ files covering config, tools, agents, hooks, commands, state, guardrails, evidence, plan schemas, circuit breaker race conditions, invocation windows, multi-invocation isolation, security categories, review/integration schemas, and diff tool. Uses Bun's built-in test runner β€” zero additional test dependencies.
586
634
 
587
635
  ## Troubleshooting
588
636
 
@@ -20,6 +20,6 @@ export { createArchitectAgent } from './architect';
20
20
  export { createCoderAgent } from './coder';
21
21
  export { createCriticAgent } from './critic';
22
22
  export { createExplorerAgent } from './explorer';
23
- export { createReviewerAgent } from './reviewer';
23
+ export { createReviewerAgent, SECURITY_CATEGORIES, type SecurityCategory, } from './reviewer';
24
24
  export { createSMEAgent } from './sme';
25
25
  export { createTestEngineerAgent } from './test-engineer';
@@ -1,2 +1,5 @@
1
1
  import type { AgentDefinition } from './architect';
2
+ /** OWASP Top 10 2021 categories for security-focused review passes */
3
+ export declare const SECURITY_CATEGORIES: readonly ["broken-access-control", "cryptographic-failures", "injection", "insecure-design", "security-misconfiguration", "vulnerable-components", "auth-failures", "data-integrity-failures", "logging-monitoring-failures", "ssrf"];
4
+ export type SecurityCategory = (typeof SECURITY_CATEGORIES)[number];
2
5
  export declare function createReviewerAgent(model: string, customPrompt?: string, customAppendPrompt?: string): AgentDefinition;
@@ -13,6 +13,8 @@ export declare function deepMerge<T extends Record<string, unknown>>(base?: T, o
13
13
  * 2. Project config: <directory>/.opencode/opencode-swarm.json
14
14
  *
15
15
  * Project config takes precedence. Nested objects are deep-merged.
16
+ * IMPORTANT: Raw configs are merged BEFORE Zod parsing so that
17
+ * Zod defaults don't override explicit user values.
16
18
  */
17
19
  export declare function loadPluginConfig(directory: string): PluginConfig;
18
20
  /**
@@ -128,6 +128,15 @@ export declare const SummaryConfigSchema: z.ZodObject<{
128
128
  retention_days: z.ZodDefault<z.ZodNumber>;
129
129
  }, z.core.$strip>;
130
130
  export type SummaryConfig = z.infer<typeof SummaryConfigSchema>;
131
+ export declare const ReviewPassesConfigSchema: z.ZodObject<{
132
+ always_security_review: z.ZodDefault<z.ZodBoolean>;
133
+ security_globs: z.ZodDefault<z.ZodArray<z.ZodString>>;
134
+ }, z.core.$strip>;
135
+ export type ReviewPassesConfig = z.infer<typeof ReviewPassesConfigSchema>;
136
+ export declare const IntegrationAnalysisConfigSchema: z.ZodObject<{
137
+ enabled: z.ZodDefault<z.ZodBoolean>;
138
+ }, z.core.$strip>;
139
+ export type IntegrationAnalysisConfig = z.infer<typeof IntegrationAnalysisConfigSchema>;
131
140
  export declare const GuardrailsProfileSchema: z.ZodObject<{
132
141
  max_tool_calls: z.ZodOptional<z.ZodNumber>;
133
142
  max_duration_minutes: z.ZodOptional<z.ZodNumber>;
@@ -282,6 +291,14 @@ export declare const PluginConfigSchema: z.ZodObject<{
282
291
  max_stored_bytes: z.ZodDefault<z.ZodNumber>;
283
292
  retention_days: z.ZodDefault<z.ZodNumber>;
284
293
  }, z.core.$strip>>;
294
+ review_passes: z.ZodOptional<z.ZodObject<{
295
+ always_security_review: z.ZodDefault<z.ZodBoolean>;
296
+ security_globs: z.ZodDefault<z.ZodArray<z.ZodString>>;
297
+ }, z.core.$strip>>;
298
+ integration_analysis: z.ZodOptional<z.ZodObject<{
299
+ enabled: z.ZodDefault<z.ZodBoolean>;
300
+ }, z.core.$strip>>;
301
+ _loadedFromFile: z.ZodDefault<z.ZodBoolean>;
285
302
  }, z.core.$strip>;
286
303
  export type PluginConfig = z.infer<typeof PluginConfigSchema>;
287
304
  export type { AgentName, PipelineAgentName, QAAgentName, } from './constants';
@@ -8,7 +8,7 @@ import type { PluginConfig } from '../config/schema';
8
8
  /**
9
9
  * Creates the chat.message hook for delegation tracking.
10
10
  */
11
- export declare function createDelegationTrackerHook(config: PluginConfig): (input: {
11
+ export declare function createDelegationTrackerHook(config: PluginConfig, guardrailsEnabled?: boolean): (input: {
12
12
  sessionID: string;
13
13
  agent?: string;
14
14
  }, output: Record<string, unknown>) => Promise<void>;
package/dist/index.js CHANGED
@@ -13630,6 +13630,21 @@ var SummaryConfigSchema = exports_external.object({
13630
13630
  max_stored_bytes: exports_external.number().min(10240).max(104857600).default(10485760),
13631
13631
  retention_days: exports_external.number().min(1).max(365).default(7)
13632
13632
  });
13633
+ var ReviewPassesConfigSchema = exports_external.object({
13634
+ always_security_review: exports_external.boolean().default(false),
13635
+ security_globs: exports_external.array(exports_external.string()).default([
13636
+ "**/auth/**",
13637
+ "**/api/**",
13638
+ "**/crypto/**",
13639
+ "**/security/**",
13640
+ "**/middleware/**",
13641
+ "**/session/**",
13642
+ "**/token/**"
13643
+ ])
13644
+ });
13645
+ var IntegrationAnalysisConfigSchema = exports_external.object({
13646
+ enabled: exports_external.boolean().default(true)
13647
+ });
13633
13648
  var GuardrailsProfileSchema = exports_external.object({
13634
13649
  max_tool_calls: exports_external.number().min(0).max(1000).optional(),
13635
13650
  max_duration_minutes: exports_external.number().min(0).max(480).optional(),
@@ -13729,7 +13744,10 @@ var PluginConfigSchema = exports_external.object({
13729
13744
  context_budget: ContextBudgetConfigSchema.optional(),
13730
13745
  guardrails: GuardrailsConfigSchema.optional(),
13731
13746
  evidence: EvidenceConfigSchema.optional(),
13732
- summaries: SummaryConfigSchema.optional()
13747
+ summaries: SummaryConfigSchema.optional(),
13748
+ review_passes: ReviewPassesConfigSchema.optional(),
13749
+ integration_analysis: IntegrationAnalysisConfigSchema.optional(),
13750
+ _loadedFromFile: exports_external.boolean().default(false)
13733
13751
  });
13734
13752
 
13735
13753
  // src/config/loader.ts
@@ -13739,25 +13757,26 @@ var MAX_CONFIG_FILE_BYTES = 102400;
13739
13757
  function getUserConfigDir() {
13740
13758
  return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
13741
13759
  }
13742
- function loadConfigFromPath(configPath) {
13760
+ function loadRawConfigFromPath(configPath) {
13743
13761
  try {
13744
13762
  const stats = fs.statSync(configPath);
13745
13763
  if (stats.size > MAX_CONFIG_FILE_BYTES) {
13746
13764
  console.warn(`[opencode-swarm] Config file too large (max 100 KB): ${configPath}`);
13765
+ console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
13747
13766
  return null;
13748
13767
  }
13749
13768
  const content = fs.readFileSync(configPath, "utf-8");
13750
13769
  const rawConfig = JSON.parse(content);
13751
- const result = PluginConfigSchema.safeParse(rawConfig);
13752
- if (!result.success) {
13753
- console.warn(`[opencode-swarm] Invalid config at ${configPath}:`);
13754
- console.warn(result.error.format());
13770
+ if (typeof rawConfig !== "object" || rawConfig === null || Array.isArray(rawConfig)) {
13771
+ console.warn(`[opencode-swarm] Invalid config at ${configPath}: expected an object`);
13772
+ console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
13755
13773
  return null;
13756
13774
  }
13757
- return result.data;
13775
+ return rawConfig;
13758
13776
  } catch (error48) {
13759
13777
  if (error48 instanceof Error && "code" in error48 && error48.code !== "ENOENT") {
13760
- console.warn(`[opencode-swarm] Error reading config from ${configPath}:`, error48.message);
13778
+ console.warn(`[opencode-swarm] \u26A0\uFE0F CONFIG LOAD FAILURE \u2014 config exists at ${configPath} but could not be loaded: ${error48.message}`);
13779
+ console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
13761
13780
  }
13762
13781
  return null;
13763
13782
  }
@@ -13779,30 +13798,36 @@ function deepMergeInternal(base, override, depth) {
13779
13798
  }
13780
13799
  return result;
13781
13800
  }
13782
- function deepMerge(base, override) {
13783
- if (!base)
13784
- return override;
13785
- if (!override)
13786
- return base;
13787
- return deepMergeInternal(base, override, 0);
13788
- }
13789
13801
  function loadPluginConfig(directory) {
13790
13802
  const userConfigPath = path.join(getUserConfigDir(), "opencode", CONFIG_FILENAME);
13791
13803
  const projectConfigPath = path.join(directory, ".opencode", CONFIG_FILENAME);
13792
- let config2 = loadConfigFromPath(userConfigPath) ?? {
13793
- max_iterations: 5,
13794
- qa_retry_limit: 3,
13795
- inject_phase_reminders: true
13796
- };
13797
- const projectConfig = loadConfigFromPath(projectConfigPath);
13798
- if (projectConfig) {
13799
- config2 = {
13800
- ...config2,
13801
- ...projectConfig,
13802
- agents: deepMerge(config2.agents, projectConfig.agents)
13804
+ const rawUserConfig = loadRawConfigFromPath(userConfigPath);
13805
+ const rawProjectConfig = loadRawConfigFromPath(projectConfigPath);
13806
+ const loadedFromFile = rawUserConfig !== null || rawProjectConfig !== null;
13807
+ let mergedRaw = rawUserConfig ?? {};
13808
+ if (rawProjectConfig) {
13809
+ mergedRaw = deepMergeInternal(mergedRaw, rawProjectConfig, 0);
13810
+ }
13811
+ const result = PluginConfigSchema.safeParse(mergedRaw);
13812
+ if (!result.success) {
13813
+ if (rawUserConfig) {
13814
+ const userResult = PluginConfigSchema.safeParse(rawUserConfig);
13815
+ if (userResult.success) {
13816
+ console.warn("[opencode-swarm] Project config ignored due to validation errors. Using user config.");
13817
+ return { ...userResult.data, _loadedFromFile: true };
13818
+ }
13819
+ }
13820
+ console.warn("[opencode-swarm] Merged config validation failed:");
13821
+ console.warn(result.error.format());
13822
+ console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
13823
+ return {
13824
+ max_iterations: 5,
13825
+ qa_retry_limit: 3,
13826
+ inject_phase_reminders: true,
13827
+ _loadedFromFile: false
13803
13828
  };
13804
13829
  }
13805
- return config2;
13830
+ return { ...result.data, _loadedFromFile: loadedFromFile };
13806
13831
  }
13807
13832
  function loadAgentPrompt(agentName) {
13808
13833
  const promptsDir = path.join(getUserConfigDir(), "opencode", PROMPTS_DIR_NAME);
@@ -14021,17 +14046,13 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
14021
14046
  - If NEEDS_REVISION: Revise plan and re-submit to critic (max 2 cycles)
14022
14047
  - If REJECTED after 2 cycles: Escalate to user with explanation
14023
14048
  - ONLY AFTER critic approval: Proceed to implementation (Phase 3+)
14024
- 7. **MANDATORY QA GATE (Execute AFTER every coder task)**:
14025
- - Step A: {{AGENT_PREFIX}}coder completes implementation \u2192 STOP
14026
- - Step B: IMMEDIATELY delegate to {{AGENT_PREFIX}}reviewer with CHECK dimensions (security, correctness, edge-cases, etc.)
14027
- - Step C: Wait for reviewer verdict
14028
- - If VERDICT: REJECTED \u2192 Send FIXES back to {{AGENT_PREFIX}}coder (return to Step A)
14029
- - If VERDICT: APPROVED \u2192 Proceed to Step D
14030
- - Step D: IMMEDIATELY delegate to {{AGENT_PREFIX}}test_engineer to generate and run tests
14031
- - Step E: Wait for test verdict
14032
- - If VERDICT: FAIL \u2192 Send failure details back to {{AGENT_PREFIX}}coder (return to Step A)
14033
- - If VERDICT: PASS \u2192 Mark task complete, proceed to next task
14034
- 8. **NEVER skip the QA gate**: You cannot delegate to {{AGENT_PREFIX}}coder for a new task until the previous task passes BOTH reviewer approval AND test_engineer verification. The sequence is ALWAYS: coder \u2192 reviewer \u2192 test_engineer \u2192 next_coder.
14049
+ 7. **MANDATORY QA GATE (Execute AFTER every coder task)** \u2014 sequence: coder \u2192 diff \u2192 review \u2192 security review \u2192 verification tests \u2192 adversarial tests \u2192 next task.
14050
+ - After coder completes: run \`diff\` tool. If \`hasContractChanges\` is true \u2192 delegate {{AGENT_PREFIX}}explorer for integration impact analysis. BREAKING \u2192 return to coder. COMPATIBLE \u2192 proceed.
14051
+ - Delegate {{AGENT_PREFIX}}reviewer with CHECK dimensions. REJECTED \u2192 return to coder (max {{QA_RETRY_LIMIT}} attempts). APPROVED \u2192 continue.
14052
+ - If file matches security globs (auth, api, crypto, security, middleware, session, token) OR coder output contains security keywords \u2192 delegate {{AGENT_PREFIX}}reviewer AGAIN with security-only CHECK. REJECTED \u2192 return to coder.
14053
+ - Delegate {{AGENT_PREFIX}}test_engineer for verification tests. FAIL \u2192 return to coder.
14054
+ - Delegate {{AGENT_PREFIX}}test_engineer for adversarial tests (attack vectors only). FAIL \u2192 return to coder.
14055
+ - All pass \u2192 mark task complete, proceed to next task.
14035
14056
 
14036
14057
  ## AGENTS
14037
14058
 
@@ -14044,6 +14065,8 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
14044
14065
 
14045
14066
  SMEs advise only. Reviewer and critic review only. None of them write code.
14046
14067
 
14068
+ Available Tools: diff (structured git diff with contract change detection)
14069
+
14047
14070
  ## DELEGATION FORMAT
14048
14071
 
14049
14072
  All delegations use this structure:
@@ -14099,6 +14122,24 @@ PLAN: [paste the plan.md content]
14099
14122
  CONTEXT: [codebase summary from explorer]
14100
14123
  OUTPUT: VERDICT + CONFIDENCE + ISSUES + SUMMARY
14101
14124
 
14125
+ {{AGENT_PREFIX}}reviewer
14126
+ TASK: Security-only review of login validation
14127
+ FILE: src/auth/login.ts
14128
+ CHECK: [security-only] \u2014 evaluate against OWASP Top 10, scan for hardcoded secrets, injection vectors, insecure crypto, missing input validation
14129
+ OUTPUT: VERDICT + RISK + SECURITY ISSUES ONLY
14130
+
14131
+ {{AGENT_PREFIX}}test_engineer
14132
+ TASK: Adversarial security testing
14133
+ FILE: src/auth/login.ts
14134
+ CONSTRAINT: ONLY attack vectors \u2014 malformed inputs, oversized payloads, injection attempts, auth bypass, boundary violations
14135
+ OUTPUT: Test file + VERDICT: PASS/FAIL
14136
+
14137
+ {{AGENT_PREFIX}}explorer
14138
+ TASK: Integration impact analysis
14139
+ INPUT: Contract changes detected: [list from diff tool]
14140
+ OUTPUT: BREAKING CHANGES + CONSUMERS AFFECTED + VERDICT: BREAKING/COMPATIBLE
14141
+ CONSTRAINT: Read-only. grep for imports/usages of changed exports.
14142
+
14102
14143
  ## WORKFLOW
14103
14144
 
14104
14145
  ### Phase 0: Resume Check
@@ -14150,15 +14191,13 @@ Delegate plan to {{AGENT_PREFIX}}critic for review BEFORE any implementation beg
14150
14191
  ### Phase 5: Execute
14151
14192
  For each task (respecting dependencies):
14152
14193
 
14153
- 5a. {{AGENT_PREFIX}}coder - Implement (MANDATORY)
14154
- 5b. {{AGENT_PREFIX}}reviewer - Review (specify CHECK dimensions relevant to the change)
14155
- 5c. **GATE - Check VERDICT:**
14156
- - **APPROVED** \u2192 Proceed to 5d
14157
- - **REJECTED** (attempt < {{QA_RETRY_LIMIT}}) \u2192 STOP. Send FIXES to {{AGENT_PREFIX}}coder with specific changes. Retry from 5a. Do NOT proceed to 5d.
14158
- - **REJECTED** (attempt {{QA_RETRY_LIMIT}}) \u2192 STOP. Escalate to user or handle directly.
14159
- 5d. {{AGENT_PREFIX}}test_engineer - Generate AND run tests (ONLY if 5c = APPROVED). Expect VERDICT: PASS/FAIL.
14160
- 5e. If test VERDICT is FAIL \u2192 Send failures to {{AGENT_PREFIX}}coder for fixes, then re-run from 5b.
14161
- 5f. Update plan.md [x], proceed to next task (ONLY if tests PASS)
14194
+ 5a. {{AGENT_PREFIX}}coder - Implement
14195
+ 5b. Run \`diff\` tool. If \`hasContractChanges\` \u2192 {{AGENT_PREFIX}}explorer integration analysis. BREAKING \u2192 coder retry.
14196
+ 5c. {{AGENT_PREFIX}}reviewer - General review. REJECTED (< {{QA_RETRY_LIMIT}}) \u2192 coder retry. REJECTED ({{QA_RETRY_LIMIT}}) \u2192 escalate.
14197
+ 5d. Security gate: if file matches security globs or content has security keywords \u2192 {{AGENT_PREFIX}}reviewer security-only. REJECTED \u2192 coder retry.
14198
+ 5e. {{AGENT_PREFIX}}test_engineer - Verification tests. FAIL \u2192 coder retry from 5c.
14199
+ 5f. {{AGENT_PREFIX}}test_engineer - Adversarial tests. FAIL \u2192 coder retry from 5c.
14200
+ 5g. Update plan.md [x], proceed to next task.
14162
14201
 
14163
14202
  ### Phase 6: Phase Complete
14164
14203
  1. {{AGENT_PREFIX}}explorer - Rescan
@@ -16828,7 +16867,7 @@ ${originalText}`;
16828
16867
  };
16829
16868
  }
16830
16869
  // src/hooks/delegation-tracker.ts
16831
- function createDelegationTrackerHook(config2) {
16870
+ function createDelegationTrackerHook(config2, guardrailsEnabled = true) {
16832
16871
  return async (input, _output) => {
16833
16872
  const now = Date.now();
16834
16873
  if (!input.agent || input.agent === "") {
@@ -16850,7 +16889,7 @@ function createDelegationTrackerHook(config2) {
16850
16889
  const isArchitect = strippedAgent === ORCHESTRATOR_NAME;
16851
16890
  const session = ensureAgentSession(input.sessionID, agentName);
16852
16891
  session.delegationActive = !isArchitect;
16853
- if (!isArchitect) {
16892
+ if (!isArchitect && guardrailsEnabled) {
16854
16893
  beginInvocation(input.sessionID, agentName);
16855
16894
  }
16856
16895
  if (config2.hooks?.delegation_tracker === true && previousAgent && previousAgent !== agentName) {
@@ -17025,18 +17064,11 @@ function createGuardrailsHooks(config2) {
17025
17064
  return;
17026
17065
  }
17027
17066
  const lastMessage = messages[messages.length - 1];
17028
- let sessionId = lastMessage.info?.sessionID;
17029
- let targetWindow = sessionId ? getActiveWindow(sessionId) : undefined;
17030
- if (!targetWindow || !targetWindow.warningIssued && !targetWindow.hardLimitHit) {
17031
- for (const [id] of swarmState.agentSessions) {
17032
- const window = getActiveWindow(id);
17033
- if (window && (window.warningIssued || window.hardLimitHit)) {
17034
- targetWindow = window;
17035
- sessionId = id;
17036
- break;
17037
- }
17038
- }
17067
+ const sessionId = lastMessage.info?.sessionID;
17068
+ if (!sessionId) {
17069
+ return;
17039
17070
  }
17071
+ const targetWindow = getActiveWindow(sessionId);
17040
17072
  if (!targetWindow || !targetWindow.warningIssued && !targetWindow.hardLimitHit) {
17041
17073
  return;
17042
17074
  }
@@ -17244,6 +17276,12 @@ function createSystemEnhancerHook(config2, directory) {
17244
17276
  }
17245
17277
  }
17246
17278
  tryInject("[SWARM HINT] Large tool outputs may be auto-summarized. Use /swarm retrieve <id> to get the full content if needed.");
17279
+ if (config2.review_passes?.always_security_review) {
17280
+ tryInject("[SWARM CONFIG] Security review pass is MANDATORY for ALL tasks. Skip file-pattern check \u2014 always run security-only reviewer pass after general review APPROVED.");
17281
+ }
17282
+ if (config2.integration_analysis?.enabled === false) {
17283
+ tryInject("[SWARM CONFIG] Integration analysis is DISABLED. Skip diff tool and integration impact analysis after coder tasks.");
17284
+ }
17247
17285
  return;
17248
17286
  }
17249
17287
  const userScoringConfig = config2.context_budget?.scoring;
@@ -17323,6 +17361,28 @@ function createSystemEnhancerHook(config2, directory) {
17323
17361
  }
17324
17362
  }
17325
17363
  }
17364
+ if (config2.review_passes?.always_security_review) {
17365
+ const text = "[SWARM CONFIG] Security review pass is MANDATORY for ALL tasks. Skip file-pattern check \u2014 always run security-only reviewer pass after general review APPROVED.";
17366
+ candidates.push({
17367
+ id: `candidate-${idCounter++}`,
17368
+ kind: "phase",
17369
+ text,
17370
+ tokens: estimateTokens(text),
17371
+ priority: 1,
17372
+ metadata: { contentType: "prose" }
17373
+ });
17374
+ }
17375
+ if (config2.integration_analysis?.enabled === false) {
17376
+ const text = "[SWARM CONFIG] Integration analysis is DISABLED. Skip diff tool and integration impact analysis after coder tasks.";
17377
+ candidates.push({
17378
+ id: `candidate-${idCounter++}`,
17379
+ kind: "phase",
17380
+ text,
17381
+ tokens: estimateTokens(text),
17382
+ priority: 1,
17383
+ metadata: { contentType: "prose" }
17384
+ });
17385
+ }
17326
17386
  const ranked = rankCandidates(candidates, effectiveConfig);
17327
17387
  for (const candidate of ranked) {
17328
17388
  if (injectedTokens + candidate.tokens > maxInjectionTokens) {
@@ -17517,6 +17577,9 @@ function createToolSummarizerHook(config2, directory) {
17517
17577
  }
17518
17578
  };
17519
17579
  }
17580
+ // src/tools/diff.ts
17581
+ import { execSync } from "child_process";
17582
+
17520
17583
  // node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js
17521
17584
  var exports_external2 = {};
17522
17585
  __export(exports_external2, {
@@ -29837,7 +29900,149 @@ function tool(input) {
29837
29900
  return input;
29838
29901
  }
29839
29902
  tool.schema = exports_external2;
29840
-
29903
+ // src/tools/diff.ts
29904
+ var MAX_DIFF_LINES = 500;
29905
+ var DIFF_TIMEOUT_MS = 30000;
29906
+ var MAX_BUFFER_BYTES = 5 * 1024 * 1024;
29907
+ var CONTRACT_PATTERNS = [
29908
+ /^[+-]\s*export\s+(function|const|class|interface|type|enum|default)\b/,
29909
+ /^[+-]\s*(interface|type)\s+\w+/,
29910
+ /^[+-]\s*public\s+/,
29911
+ /^[+-]\s*(async\s+)?function\s+\w+\s*\(/
29912
+ ];
29913
+ var SAFE_REF_PATTERN = /^[a-zA-Z0-9._\-/~^@{}]+$/;
29914
+ var MAX_REF_LENGTH = 256;
29915
+ var MAX_PATH_LENGTH = 500;
29916
+ var SHELL_METACHARACTERS = /[;|&$`(){}<>!'"]/;
29917
+ function validateBase(base) {
29918
+ if (base.length > MAX_REF_LENGTH) {
29919
+ return `base ref exceeds maximum length of ${MAX_REF_LENGTH}`;
29920
+ }
29921
+ if (!SAFE_REF_PATTERN.test(base)) {
29922
+ return "base contains invalid characters for git ref";
29923
+ }
29924
+ return null;
29925
+ }
29926
+ function validatePaths(paths) {
29927
+ if (!paths)
29928
+ return null;
29929
+ for (const path7 of paths) {
29930
+ if (!path7 || path7.length === 0) {
29931
+ return "empty path not allowed";
29932
+ }
29933
+ if (path7.length > MAX_PATH_LENGTH) {
29934
+ return `path exceeds maximum length of ${MAX_PATH_LENGTH}`;
29935
+ }
29936
+ if (SHELL_METACHARACTERS.test(path7)) {
29937
+ return "path contains shell metacharacters";
29938
+ }
29939
+ }
29940
+ return null;
29941
+ }
29942
+ var diff = tool({
29943
+ description: "Analyze git diff for changed files, exports, interfaces, and function signatures. Returns structured output with contract change detection.",
29944
+ args: {
29945
+ base: tool.schema.string().optional().describe('Base ref to diff against (default: HEAD). Use "staged" for staged changes, "unstaged" for working tree changes.'),
29946
+ paths: tool.schema.array(tool.schema.string()).optional().describe("Optional file paths to restrict diff scope.")
29947
+ },
29948
+ async execute(args, _context) {
29949
+ try {
29950
+ const base = args.base ?? "HEAD";
29951
+ const pathSpec = args.paths?.length ? "-- " + args.paths.join(" ") : "";
29952
+ const baseValidationError = validateBase(base);
29953
+ if (baseValidationError) {
29954
+ const errorResult = {
29955
+ error: `invalid base: ${baseValidationError}`,
29956
+ files: [],
29957
+ contractChanges: [],
29958
+ hasContractChanges: false
29959
+ };
29960
+ return JSON.stringify(errorResult, null, 2);
29961
+ }
29962
+ const pathsValidationError = validatePaths(args.paths);
29963
+ if (pathsValidationError) {
29964
+ const errorResult = {
29965
+ error: `invalid paths: ${pathsValidationError}`,
29966
+ files: [],
29967
+ contractChanges: [],
29968
+ hasContractChanges: false
29969
+ };
29970
+ return JSON.stringify(errorResult, null, 2);
29971
+ }
29972
+ let gitCmd;
29973
+ if (base === "staged") {
29974
+ gitCmd = "git --no-pager diff --cached";
29975
+ } else if (base === "unstaged") {
29976
+ gitCmd = "git --no-pager diff";
29977
+ } else {
29978
+ gitCmd = `git --no-pager diff ${base}`;
29979
+ }
29980
+ const numstatOutput = execSync(gitCmd + " --numstat " + pathSpec, {
29981
+ encoding: "utf-8",
29982
+ timeout: DIFF_TIMEOUT_MS
29983
+ });
29984
+ const fullDiffOutput = execSync(gitCmd + " -U3 " + pathSpec, {
29985
+ encoding: "utf-8",
29986
+ timeout: DIFF_TIMEOUT_MS,
29987
+ maxBuffer: MAX_BUFFER_BYTES
29988
+ });
29989
+ const files = [];
29990
+ const numstatLines = numstatOutput.split(`
29991
+ `);
29992
+ for (const line of numstatLines) {
29993
+ if (!line.trim())
29994
+ continue;
29995
+ const parts = line.split("\t");
29996
+ if (parts.length >= 3) {
29997
+ const additions = parseInt(parts[0]) || 0;
29998
+ const deletions = parseInt(parts[1]) || 0;
29999
+ const path7 = parts[2];
30000
+ files.push({ path: path7, additions, deletions });
30001
+ }
30002
+ }
30003
+ const contractChanges = [];
30004
+ const diffLines = fullDiffOutput.split(`
30005
+ `);
30006
+ let currentFile = "";
30007
+ for (const line of diffLines) {
30008
+ const gitLineMatch = line.match(/^diff --git.* b\/(.+)$/);
30009
+ if (gitLineMatch) {
30010
+ currentFile = gitLineMatch[1];
30011
+ }
30012
+ for (const pattern of CONTRACT_PATTERNS) {
30013
+ if (pattern.test(line)) {
30014
+ const trimmed = line.trim();
30015
+ if (currentFile) {
30016
+ contractChanges.push(`[${currentFile}] ${trimmed}`);
30017
+ } else {
30018
+ contractChanges.push(trimmed);
30019
+ }
30020
+ break;
30021
+ }
30022
+ }
30023
+ }
30024
+ const hasContractChanges = contractChanges.length > 0;
30025
+ const fileCount = files.length;
30026
+ const truncated = diffLines.length > MAX_DIFF_LINES;
30027
+ const summary = truncated ? `${fileCount} files changed. Contract changes: ${hasContractChanges ? "YES" : "NO"}. (truncated to ${MAX_DIFF_LINES} lines)` : `${fileCount} files changed. Contract changes: ${hasContractChanges ? "YES" : "NO"}`;
30028
+ const result = {
30029
+ files,
30030
+ contractChanges,
30031
+ hasContractChanges,
30032
+ summary
30033
+ };
30034
+ return JSON.stringify(result, null, 2);
30035
+ } catch (e) {
30036
+ const errorResult = {
30037
+ error: e instanceof Error ? `git diff failed: ${e.constructor.name}` : "git diff failed: unknown error",
30038
+ files: [],
30039
+ contractChanges: [],
30040
+ hasContractChanges: false
30041
+ };
30042
+ return JSON.stringify(errorResult, null, 2);
30043
+ }
30044
+ }
30045
+ });
29841
30046
  // src/tools/domain-detector.ts
29842
30047
  var DOMAIN_PATTERNS = {
29843
30048
  windows: [
@@ -30222,9 +30427,10 @@ var OpenCodeSwarm = async (ctx) => {
30222
30427
  const contextBudgetHandler = createContextBudgetHandler(config3);
30223
30428
  const commandHandler = createSwarmCommandHandler(ctx.directory, Object.fromEntries(agentDefinitions.map((agent) => [agent.name, agent])));
30224
30429
  const activityHooks = createAgentActivityHooks(config3, ctx.directory);
30225
- const delegationHandler = createDelegationTrackerHook(config3);
30226
30430
  const delegationGateHandler = createDelegationGateHook(config3);
30227
- const guardrailsConfig = GuardrailsConfigSchema.parse(config3.guardrails ?? {});
30431
+ const guardrailsFallback = config3._loadedFromFile ? config3.guardrails ?? {} : { ...config3.guardrails, enabled: false };
30432
+ const guardrailsConfig = GuardrailsConfigSchema.parse(guardrailsFallback);
30433
+ const delegationHandler = createDelegationTrackerHook(config3, guardrailsConfig.enabled);
30228
30434
  const guardrailsHooks = createGuardrailsHooks(guardrailsConfig);
30229
30435
  const summaryConfig = SummaryConfigSchema.parse(config3.summaries ?? {});
30230
30436
  const toolSummarizerHook = createToolSummarizerHook(summaryConfig, ctx.directory);
@@ -30251,7 +30457,8 @@ var OpenCodeSwarm = async (ctx) => {
30251
30457
  tool: {
30252
30458
  detect_domains,
30253
30459
  extract_code_blocks,
30254
- gitingest
30460
+ gitingest,
30461
+ diff
30255
30462
  },
30256
30463
  config: async (opencodeConfig) => {
30257
30464
  if (!opencodeConfig.agent) {
@@ -0,0 +1,18 @@
1
+ import { tool } from '@opencode-ai/plugin';
2
+ export interface DiffResult {
3
+ files: Array<{
4
+ path: string;
5
+ additions: number;
6
+ deletions: number;
7
+ }>;
8
+ contractChanges: string[];
9
+ hasContractChanges: boolean;
10
+ summary: string;
11
+ }
12
+ export interface DiffErrorResult {
13
+ error: string;
14
+ files: [];
15
+ contractChanges: [];
16
+ hasContractChanges: false;
17
+ }
18
+ export declare const diff: ReturnType<typeof tool>;
@@ -1,3 +1,4 @@
1
+ export { type DiffErrorResult, type DiffResult, diff } from './diff';
1
2
  export { detect_domains } from './domain-detector';
2
3
  export { extract_code_blocks } from './file-extractor';
3
- export { gitingest, fetchGitingest, type GitingestArgs } from './gitingest';
4
+ export { fetchGitingest, type GitingestArgs, gitingest } from './gitingest';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "5.2.0",
3
+ "version": "6.0.1",
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",