opencode-swarm 5.2.0 β†’ 6.1.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 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
 
@@ -0,0 +1,2 @@
1
+ import type { AgentDefinition } from './architect';
2
+ export declare function createDesignerAgent(model: string, customPrompt?: string, customAppendPrompt?: string): AgentDefinition;
@@ -0,0 +1,2 @@
1
+ import type { AgentDefinition } from './architect';
2
+ export declare function createDocsAgent(model: string, customPrompt?: string, customAppendPrompt?: string): AgentDefinition;
@@ -19,7 +19,9 @@ export declare function getAgentConfigs(config?: PluginConfig): Record<string, S
19
19
  export { createArchitectAgent } from './architect';
20
20
  export { createCoderAgent } from './coder';
21
21
  export { createCriticAgent } from './critic';
22
+ export { createDesignerAgent } from './designer';
23
+ export { createDocsAgent } from './docs';
22
24
  export { createExplorerAgent } from './explorer';
23
- export { createReviewerAgent } from './reviewer';
25
+ export { createReviewerAgent, SECURITY_CATEGORIES, type SecurityCategory, } from './reviewer';
24
26
  export { createSMEAgent } from './sme';
25
27
  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;
@@ -1,8 +1,8 @@
1
1
  export declare const QA_AGENTS: readonly ["reviewer", "critic"];
2
2
  export declare const PIPELINE_AGENTS: readonly ["explorer", "coder", "test_engineer"];
3
3
  export declare const ORCHESTRATOR_NAME: "architect";
4
- export declare const ALL_SUBAGENT_NAMES: readonly ["sme", "reviewer", "critic", "explorer", "coder", "test_engineer"];
5
- export declare const ALL_AGENT_NAMES: readonly ["architect", "sme", "reviewer", "critic", "explorer", "coder", "test_engineer"];
4
+ export declare const ALL_SUBAGENT_NAMES: readonly ["sme", "docs", "designer", "reviewer", "critic", "explorer", "coder", "test_engineer"];
5
+ export declare const ALL_AGENT_NAMES: readonly ["architect", "sme", "docs", "designer", "reviewer", "critic", "explorer", "coder", "test_engineer"];
6
6
  export type QAAgentName = (typeof QA_AGENTS)[number];
7
7
  export type PipelineAgentName = (typeof PIPELINE_AGENTS)[number];
8
8
  export type AgentName = (typeof ALL_AGENT_NAMES)[number];
@@ -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,26 @@ 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>;
140
+ export declare const DocsConfigSchema: z.ZodObject<{
141
+ enabled: z.ZodDefault<z.ZodBoolean>;
142
+ doc_patterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
143
+ }, z.core.$strip>;
144
+ export type DocsConfig = z.infer<typeof DocsConfigSchema>;
145
+ export declare const UIReviewConfigSchema: z.ZodObject<{
146
+ enabled: z.ZodDefault<z.ZodBoolean>;
147
+ trigger_paths: z.ZodDefault<z.ZodArray<z.ZodString>>;
148
+ trigger_keywords: z.ZodDefault<z.ZodArray<z.ZodString>>;
149
+ }, z.core.$strip>;
150
+ export type UIReviewConfig = z.infer<typeof UIReviewConfigSchema>;
131
151
  export declare const GuardrailsProfileSchema: z.ZodObject<{
132
152
  max_tool_calls: z.ZodOptional<z.ZodNumber>;
133
153
  max_duration_minutes: z.ZodOptional<z.ZodNumber>;
@@ -282,6 +302,23 @@ export declare const PluginConfigSchema: z.ZodObject<{
282
302
  max_stored_bytes: z.ZodDefault<z.ZodNumber>;
283
303
  retention_days: z.ZodDefault<z.ZodNumber>;
284
304
  }, z.core.$strip>>;
305
+ review_passes: z.ZodOptional<z.ZodObject<{
306
+ always_security_review: z.ZodDefault<z.ZodBoolean>;
307
+ security_globs: z.ZodDefault<z.ZodArray<z.ZodString>>;
308
+ }, z.core.$strip>>;
309
+ integration_analysis: z.ZodOptional<z.ZodObject<{
310
+ enabled: z.ZodDefault<z.ZodBoolean>;
311
+ }, z.core.$strip>>;
312
+ docs: z.ZodOptional<z.ZodObject<{
313
+ enabled: z.ZodDefault<z.ZodBoolean>;
314
+ doc_patterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
315
+ }, z.core.$strip>>;
316
+ ui_review: z.ZodOptional<z.ZodObject<{
317
+ enabled: z.ZodDefault<z.ZodBoolean>;
318
+ trigger_paths: z.ZodDefault<z.ZodArray<z.ZodString>>;
319
+ trigger_keywords: z.ZodDefault<z.ZodArray<z.ZodString>>;
320
+ }, z.core.$strip>>;
321
+ _loadedFromFile: z.ZodDefault<z.ZodBoolean>;
285
322
  }, z.core.$strip>;
286
323
  export type PluginConfig = z.infer<typeof PluginConfigSchema>;
287
324
  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,61 @@ 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
+ });
13648
+ var DocsConfigSchema = exports_external.object({
13649
+ enabled: exports_external.boolean().default(true),
13650
+ doc_patterns: exports_external.array(exports_external.string()).default([
13651
+ "README.md",
13652
+ "CONTRIBUTING.md",
13653
+ "docs/**/*.md",
13654
+ "docs/**/*.rst",
13655
+ "**/CHANGELOG.md"
13656
+ ])
13657
+ });
13658
+ var UIReviewConfigSchema = exports_external.object({
13659
+ enabled: exports_external.boolean().default(false),
13660
+ trigger_paths: exports_external.array(exports_external.string()).default([
13661
+ "**/pages/**",
13662
+ "**/components/**",
13663
+ "**/views/**",
13664
+ "**/screens/**",
13665
+ "**/ui/**",
13666
+ "**/layouts/**"
13667
+ ]),
13668
+ trigger_keywords: exports_external.array(exports_external.string()).default([
13669
+ "new page",
13670
+ "new screen",
13671
+ "new component",
13672
+ "redesign",
13673
+ "layout change",
13674
+ "form",
13675
+ "modal",
13676
+ "dialog",
13677
+ "dropdown",
13678
+ "sidebar",
13679
+ "navbar",
13680
+ "dashboard",
13681
+ "landing page",
13682
+ "signup",
13683
+ "login form",
13684
+ "settings page",
13685
+ "profile page"
13686
+ ])
13687
+ });
13633
13688
  var GuardrailsProfileSchema = exports_external.object({
13634
13689
  max_tool_calls: exports_external.number().min(0).max(1000).optional(),
13635
13690
  max_duration_minutes: exports_external.number().min(0).max(480).optional(),
@@ -13674,6 +13729,16 @@ var DEFAULT_AGENT_PROFILES = {
13674
13729
  max_tool_calls: 200,
13675
13730
  max_duration_minutes: 30,
13676
13731
  warning_threshold: 0.65
13732
+ },
13733
+ docs: {
13734
+ max_tool_calls: 200,
13735
+ max_duration_minutes: 30,
13736
+ warning_threshold: 0.75
13737
+ },
13738
+ designer: {
13739
+ max_tool_calls: 150,
13740
+ max_duration_minutes: 20,
13741
+ warning_threshold: 0.75
13677
13742
  }
13678
13743
  };
13679
13744
  var DEFAULT_ARCHITECT_PROFILE = DEFAULT_AGENT_PROFILES.architect;
@@ -13729,7 +13794,12 @@ var PluginConfigSchema = exports_external.object({
13729
13794
  context_budget: ContextBudgetConfigSchema.optional(),
13730
13795
  guardrails: GuardrailsConfigSchema.optional(),
13731
13796
  evidence: EvidenceConfigSchema.optional(),
13732
- summaries: SummaryConfigSchema.optional()
13797
+ summaries: SummaryConfigSchema.optional(),
13798
+ review_passes: ReviewPassesConfigSchema.optional(),
13799
+ integration_analysis: IntegrationAnalysisConfigSchema.optional(),
13800
+ docs: DocsConfigSchema.optional(),
13801
+ ui_review: UIReviewConfigSchema.optional(),
13802
+ _loadedFromFile: exports_external.boolean().default(false)
13733
13803
  });
13734
13804
 
13735
13805
  // src/config/loader.ts
@@ -13739,25 +13809,26 @@ var MAX_CONFIG_FILE_BYTES = 102400;
13739
13809
  function getUserConfigDir() {
13740
13810
  return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
13741
13811
  }
13742
- function loadConfigFromPath(configPath) {
13812
+ function loadRawConfigFromPath(configPath) {
13743
13813
  try {
13744
13814
  const stats = fs.statSync(configPath);
13745
13815
  if (stats.size > MAX_CONFIG_FILE_BYTES) {
13746
13816
  console.warn(`[opencode-swarm] Config file too large (max 100 KB): ${configPath}`);
13817
+ console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
13747
13818
  return null;
13748
13819
  }
13749
13820
  const content = fs.readFileSync(configPath, "utf-8");
13750
13821
  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());
13822
+ if (typeof rawConfig !== "object" || rawConfig === null || Array.isArray(rawConfig)) {
13823
+ console.warn(`[opencode-swarm] Invalid config at ${configPath}: expected an object`);
13824
+ console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
13755
13825
  return null;
13756
13826
  }
13757
- return result.data;
13827
+ return rawConfig;
13758
13828
  } catch (error48) {
13759
13829
  if (error48 instanceof Error && "code" in error48 && error48.code !== "ENOENT") {
13760
- console.warn(`[opencode-swarm] Error reading config from ${configPath}:`, error48.message);
13830
+ console.warn(`[opencode-swarm] \u26A0\uFE0F CONFIG LOAD FAILURE \u2014 config exists at ${configPath} but could not be loaded: ${error48.message}`);
13831
+ console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
13761
13832
  }
13762
13833
  return null;
13763
13834
  }
@@ -13779,30 +13850,36 @@ function deepMergeInternal(base, override, depth) {
13779
13850
  }
13780
13851
  return result;
13781
13852
  }
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
13853
  function loadPluginConfig(directory) {
13790
13854
  const userConfigPath = path.join(getUserConfigDir(), "opencode", CONFIG_FILENAME);
13791
13855
  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)
13856
+ const rawUserConfig = loadRawConfigFromPath(userConfigPath);
13857
+ const rawProjectConfig = loadRawConfigFromPath(projectConfigPath);
13858
+ const loadedFromFile = rawUserConfig !== null || rawProjectConfig !== null;
13859
+ let mergedRaw = rawUserConfig ?? {};
13860
+ if (rawProjectConfig) {
13861
+ mergedRaw = deepMergeInternal(mergedRaw, rawProjectConfig, 0);
13862
+ }
13863
+ const result = PluginConfigSchema.safeParse(mergedRaw);
13864
+ if (!result.success) {
13865
+ if (rawUserConfig) {
13866
+ const userResult = PluginConfigSchema.safeParse(rawUserConfig);
13867
+ if (userResult.success) {
13868
+ console.warn("[opencode-swarm] Project config ignored due to validation errors. Using user config.");
13869
+ return { ...userResult.data, _loadedFromFile: true };
13870
+ }
13871
+ }
13872
+ console.warn("[opencode-swarm] Merged config validation failed:");
13873
+ console.warn(result.error.format());
13874
+ console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
13875
+ return {
13876
+ max_iterations: 5,
13877
+ qa_retry_limit: 3,
13878
+ inject_phase_reminders: true,
13879
+ _loadedFromFile: false
13803
13880
  };
13804
13881
  }
13805
- return config2;
13882
+ return { ...result.data, _loadedFromFile: loadedFromFile };
13806
13883
  }
13807
13884
  function loadAgentPrompt(agentName) {
13808
13885
  const promptsDir = path.join(getUserConfigDir(), "opencode", PROMPTS_DIR_NAME);
@@ -13832,6 +13909,8 @@ var PIPELINE_AGENTS = ["explorer", "coder", "test_engineer"];
13832
13909
  var ORCHESTRATOR_NAME = "architect";
13833
13910
  var ALL_SUBAGENT_NAMES = [
13834
13911
  "sme",
13912
+ "docs",
13913
+ "designer",
13835
13914
  ...QA_AGENTS,
13836
13915
  ...PIPELINE_AGENTS
13837
13916
  ];
@@ -13847,6 +13926,8 @@ var DEFAULT_MODELS = {
13847
13926
  sme: "google/gemini-2.0-flash",
13848
13927
  reviewer: "google/gemini-2.0-flash",
13849
13928
  critic: "google/gemini-2.0-flash",
13929
+ docs: "google/gemini-2.0-flash",
13930
+ designer: "google/gemini-2.0-flash",
13850
13931
  default: "google/gemini-2.0-flash"
13851
13932
  };
13852
13933
  var DEFAULT_SCORING_CONFIG = {
@@ -13998,7 +14079,7 @@ var ARCHITECT_PROMPT = `You are Architect - orchestrator of a multi-agent swarm.
13998
14079
  ## IDENTITY
13999
14080
 
14000
14081
  Swarm: {{SWARM_ID}}
14001
- Your agents: {{AGENT_PREFIX}}explorer, {{AGENT_PREFIX}}sme, {{AGENT_PREFIX}}coder, {{AGENT_PREFIX}}reviewer, {{AGENT_PREFIX}}critic, {{AGENT_PREFIX}}test_engineer
14082
+ Your agents: {{AGENT_PREFIX}}explorer, {{AGENT_PREFIX}}sme, {{AGENT_PREFIX}}coder, {{AGENT_PREFIX}}reviewer, {{AGENT_PREFIX}}critic, {{AGENT_PREFIX}}test_engineer, {{AGENT_PREFIX}}docs, {{AGENT_PREFIX}}designer
14002
14083
 
14003
14084
  ## ROLE
14004
14085
 
@@ -14021,17 +14102,18 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
14021
14102
  - If NEEDS_REVISION: Revise plan and re-submit to critic (max 2 cycles)
14022
14103
  - If REJECTED after 2 cycles: Escalate to user with explanation
14023
14104
  - 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.
14105
+ 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.
14106
+ - 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.
14107
+ - Delegate {{AGENT_PREFIX}}reviewer with CHECK dimensions. REJECTED \u2192 return to coder (max {{QA_RETRY_LIMIT}} attempts). APPROVED \u2192 continue.
14108
+ - 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.
14109
+ - Delegate {{AGENT_PREFIX}}test_engineer for verification tests. FAIL \u2192 return to coder.
14110
+ - Delegate {{AGENT_PREFIX}}test_engineer for adversarial tests (attack vectors only). FAIL \u2192 return to coder.
14111
+ - All pass \u2192 mark task complete, proceed to next task.
14112
+ 9. **UI/UX DESIGN GATE**: Before delegating UI tasks to {{AGENT_PREFIX}}coder, check if the task involves UI components. Trigger conditions (ANY match):
14113
+ - Task description contains UI keywords: new page, new screen, new component, redesign, layout change, form, modal, dialog, dropdown, sidebar, navbar, dashboard, landing page, signup, login form, settings page, profile page
14114
+ - Target file is in: pages/, components/, views/, screens/, ui/, layouts/
14115
+ If triggered: delegate to {{AGENT_PREFIX}}designer FIRST to produce a code scaffold. Then pass the scaffold to {{AGENT_PREFIX}}coder as INPUT alongside the task. The coder implements the TODOs in the scaffold without changing component structure or accessibility attributes.
14116
+ If not triggered: delegate directly to {{AGENT_PREFIX}}coder as normal.
14035
14117
 
14036
14118
  ## AGENTS
14037
14119
 
@@ -14041,9 +14123,13 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
14041
14123
  {{AGENT_PREFIX}}reviewer - Code review (correctness, security, and any other dimensions you specify)
14042
14124
  {{AGENT_PREFIX}}test_engineer - Test generation AND execution (writes tests, runs them, reports PASS/FAIL)
14043
14125
  {{AGENT_PREFIX}}critic - Plan review gate (reviews plan BEFORE implementation)
14126
+ {{AGENT_PREFIX}}docs - Documentation updates (README, API docs, guides \u2014 NOT .swarm/ files)
14127
+ {{AGENT_PREFIX}}designer - UI/UX design specs (scaffold generation for UI components \u2014 runs BEFORE coder on UI tasks)
14044
14128
 
14045
14129
  SMEs advise only. Reviewer and critic review only. None of them write code.
14046
14130
 
14131
+ Available Tools: diff (structured git diff with contract change detection)
14132
+
14047
14133
  ## DELEGATION FORMAT
14048
14134
 
14049
14135
  All delegations use this structure:
@@ -14099,6 +14185,41 @@ PLAN: [paste the plan.md content]
14099
14185
  CONTEXT: [codebase summary from explorer]
14100
14186
  OUTPUT: VERDICT + CONFIDENCE + ISSUES + SUMMARY
14101
14187
 
14188
+ {{AGENT_PREFIX}}reviewer
14189
+ TASK: Security-only review of login validation
14190
+ FILE: src/auth/login.ts
14191
+ CHECK: [security-only] \u2014 evaluate against OWASP Top 10, scan for hardcoded secrets, injection vectors, insecure crypto, missing input validation
14192
+ OUTPUT: VERDICT + RISK + SECURITY ISSUES ONLY
14193
+
14194
+ {{AGENT_PREFIX}}test_engineer
14195
+ TASK: Adversarial security testing
14196
+ FILE: src/auth/login.ts
14197
+ CONSTRAINT: ONLY attack vectors \u2014 malformed inputs, oversized payloads, injection attempts, auth bypass, boundary violations
14198
+ OUTPUT: Test file + VERDICT: PASS/FAIL
14199
+
14200
+ {{AGENT_PREFIX}}explorer
14201
+ TASK: Integration impact analysis
14202
+ INPUT: Contract changes detected: [list from diff tool]
14203
+ OUTPUT: BREAKING CHANGES + CONSUMERS AFFECTED + VERDICT: BREAKING/COMPATIBLE
14204
+ CONSTRAINT: Read-only. grep for imports/usages of changed exports.
14205
+
14206
+ {{AGENT_PREFIX}}docs
14207
+ TASK: Update documentation for Phase 2 changes
14208
+ FILES CHANGED: src/auth/login.ts, src/auth/session.ts, src/types/user.ts
14209
+ CHANGES SUMMARY:
14210
+ - Added login() function with email/password authentication
14211
+ - Added SessionManager class with create/revoke/refresh methods
14212
+ - Added UserSession interface with refreshToken field
14213
+ DOC FILES: README.md, docs/api.md, docs/installation.md
14214
+ OUTPUT: Updated doc files + SUMMARY
14215
+
14216
+ {{AGENT_PREFIX}}designer
14217
+ TASK: Design specification for user settings page
14218
+ CONTEXT: Users need to update profile info, change password, manage notification preferences. App uses React + Tailwind + shadcn/ui.
14219
+ FRAMEWORK: React (TSX)
14220
+ EXISTING PATTERNS: All forms use react-hook-form, validation with zod, toast notifications for success/error
14221
+ OUTPUT: Code scaffold for src/pages/Settings.tsx with component tree, typed props, layout, and accessibility
14222
+
14102
14223
  ## WORKFLOW
14103
14224
 
14104
14225
  ### Phase 0: Resume Check
@@ -14150,21 +14271,24 @@ Delegate plan to {{AGENT_PREFIX}}critic for review BEFORE any implementation beg
14150
14271
  ### Phase 5: Execute
14151
14272
  For each task (respecting dependencies):
14152
14273
 
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)
14274
+ 5a. **UI DESIGN GATE** (conditional \u2014 Rule 9): If task matches UI trigger \u2192 {{AGENT_PREFIX}}designer produces scaffold \u2192 pass scaffold to coder as INPUT. If no match \u2192 skip.
14275
+ 5b. {{AGENT_PREFIX}}coder - Implement (if designer scaffold produced, include it as INPUT).
14276
+ 5c. Run \`diff\` tool. If \`hasContractChanges\` \u2192 {{AGENT_PREFIX}}explorer integration analysis. BREAKING \u2192 coder retry.
14277
+ 5d. {{AGENT_PREFIX}}reviewer - General review. REJECTED (< {{QA_RETRY_LIMIT}}) \u2192 coder retry. REJECTED ({{QA_RETRY_LIMIT}}) \u2192 escalate.
14278
+ 5e. Security gate: if file matches security globs or content has security keywords \u2192 {{AGENT_PREFIX}}reviewer security-only. REJECTED \u2192 coder retry.
14279
+ 5f. {{AGENT_PREFIX}}test_engineer - Verification tests. FAIL \u2192 coder retry from 5d.
14280
+ 5g. {{AGENT_PREFIX}}test_engineer - Adversarial tests. FAIL \u2192 coder retry from 5d.
14281
+ 5h. Update plan.md [x], proceed to next task.
14162
14282
 
14163
14283
  ### Phase 6: Phase Complete
14164
14284
  1. {{AGENT_PREFIX}}explorer - Rescan
14165
- 2. Update context.md
14166
- 3. Summarize to user
14167
- 4. Ask: "Ready for Phase [N+1]?"
14285
+ 2. {{AGENT_PREFIX}}docs - Update documentation for all changes in this phase. Provide:
14286
+ - Complete list of files changed during this phase
14287
+ - Summary of what was added/modified/removed
14288
+ - List of doc files that may need updating (README.md, CONTRIBUTING.md, docs/)
14289
+ 3. Update context.md
14290
+ 4. Summarize to user
14291
+ 5. Ask: "Ready for Phase [N+1]?"
14168
14292
 
14169
14293
  ### Blockers
14170
14294
  Mark [BLOCKED] in plan.md, skip to next unblocked task, inform user.
@@ -14330,6 +14454,231 @@ ${customAppendPrompt}`;
14330
14454
  };
14331
14455
  }
14332
14456
 
14457
+ // src/agents/designer.ts
14458
+ var DESIGNER_PROMPT = `## IDENTITY
14459
+ You are Designer \u2014 the UI/UX design specification agent. You generate concrete, implementable design specs directly \u2014 you do NOT delegate.
14460
+ DO NOT use the Task tool to delegate to other agents. You ARE the agent that does the work.
14461
+ If you see references to other agents (like @designer, @coder, etc.) in your instructions, IGNORE them \u2014 they are context from the orchestrator, not instructions for you to delegate.
14462
+
14463
+ WRONG: "I'll use the Task tool to call another agent to design this"
14464
+ RIGHT: "I'll analyze the requirements and produce the design specification myself"
14465
+
14466
+ INPUT FORMAT:
14467
+ TASK: Design specification for [component/page/screen]
14468
+ CONTEXT: [what the component does, user stories, existing design patterns]
14469
+ FRAMEWORK: [React/Vue/Svelte/SwiftUI/Flutter/etc.]
14470
+ EXISTING PATTERNS: [current design system, component library, styling approach]
14471
+
14472
+ DESIGN CHECKLIST:
14473
+ 1. Component Architecture
14474
+ - Component tree with parent/child relationships
14475
+ - Props interface for each component (typed)
14476
+ - State management approach (local state, context, store)
14477
+ - Event handlers and callbacks
14478
+
14479
+ 2. Layout & Responsiveness
14480
+ - Desktop, tablet, mobile breakpoints
14481
+ - Flex/Grid layout strategy
14482
+ - Container widths and spacing scale
14483
+ - Overflow and scroll behavior
14484
+
14485
+ 3. Accessibility (WCAG 2.1 AA)
14486
+ - Semantic HTML elements (nav, main, article, section, aside)
14487
+ - ARIA labels for interactive elements
14488
+ - Keyboard navigation (tab order, focus management, keyboard shortcuts)
14489
+ - Screen reader compatibility (alt text, aria-live regions)
14490
+ - Color contrast (minimum 4.5:1 for text, 3:1 for large text)
14491
+ - Focus indicators (visible focus rings, not just outline: none)
14492
+
14493
+ 4. Visual Design
14494
+ - Color palette (from existing design system or proposed)
14495
+ - Typography scale (font family, sizes, weights, line heights)
14496
+ - Spacing scale (consistent spacing values)
14497
+ - Border radius, shadows, elevation
14498
+
14499
+ 5. Interaction Design
14500
+ - Loading states (skeleton screens, spinners, progress bars)
14501
+ - Error states (inline validation, error boundaries, empty states)
14502
+ - Hover/focus/active states for interactive elements
14503
+ - Transitions and animations (duration, easing)
14504
+ - Optimistic updates where applicable
14505
+
14506
+ OUTPUT FORMAT:
14507
+ Produce a CODE SCAFFOLD in the target framework. This is a skeleton file with:
14508
+ - Component structure with typed props and proper imports
14509
+ - Layout structure using the project's CSS framework (Tailwind classes, CSS modules, styled-components, etc.)
14510
+ - Placeholder TODO comments for business logic
14511
+ - Accessibility attributes (aria-*, role, tabIndex)
14512
+ - Responsive breakpoint classes/media queries
14513
+ - Named event handler stubs
14514
+
14515
+ Example output structure:
14516
+ \`\`\`tsx
14517
+ // src/components/LoginForm.tsx
14518
+ // DESIGN SPEC \u2014 generated by Designer agent
14519
+ // Coder: implement TODO items, do not change component structure or accessibility attributes
14520
+
14521
+ import { useState } from 'react';
14522
+
14523
+ interface LoginFormProps {
14524
+ onSubmit: (email: string, password: string) => Promise<void>;
14525
+ onForgotPassword?: () => void;
14526
+ isLoading?: boolean;
14527
+ error?: string;
14528
+ }
14529
+
14530
+ export function LoginForm({ onSubmit, onForgotPassword, isLoading, error }: LoginFormProps) {
14531
+ const [email, setEmail] = useState('');
14532
+ const [password, setPassword] = useState('');
14533
+
14534
+ return (
14535
+ <div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 sm:px-6 lg:px-8"
14536
+ role="main">
14537
+ <div className="w-full max-w-md space-y-8">
14538
+ <div>
14539
+ <h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
14540
+ {/* TODO: Use app name from config */}
14541
+ Sign in to your account
14542
+ </h2>
14543
+ </div>
14544
+ {error && (
14545
+ <div role="alert" aria-live="polite"
14546
+ className="rounded-md bg-red-50 p-4 text-sm text-red-800">
14547
+ {error}
14548
+ </div>
14549
+ )}
14550
+ <div className="mt-8 space-y-6">
14551
+ <div className="space-y-4 rounded-md">
14552
+ <div>
14553
+ <label htmlFor="email" className="sr-only">Email address</label>
14554
+ <input id="email" name="email" type="email" autoComplete="email" required
14555
+ aria-label="Email address"
14556
+ className="relative block w-full rounded-t-md border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
14557
+ placeholder="Email address"
14558
+ value={email}
14559
+ onChange={(e) => setEmail(e.target.value)} />
14560
+ </div>
14561
+ {/* TODO: Password field with show/hide toggle */}
14562
+ {/* TODO: Remember me checkbox */}
14563
+ </div>
14564
+ <div className="flex items-center justify-between">
14565
+ {/* TODO: Forgot password link */}
14566
+ </div>
14567
+ <button type="submit" disabled={isLoading}
14568
+ aria-busy={isLoading}
14569
+ className="group relative flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:opacity-50">
14570
+ {isLoading ? 'Signing in...' : 'Sign in'}
14571
+ </button>
14572
+ </div>
14573
+ </div>
14574
+ </div>
14575
+ );
14576
+ }
14577
+ \`\`\`
14578
+
14579
+ RULES:
14580
+ - Produce REAL, syntactically valid code \u2014 not pseudocode
14581
+ - Match the project's existing framework, styling approach, and conventions
14582
+ - All interactive elements MUST have keyboard accessibility
14583
+ - All images/icons MUST have alt text or aria-label
14584
+ - Form inputs MUST have associated labels (visible or sr-only)
14585
+ - Color usage MUST meet WCAG AA contrast requirements
14586
+ - Use TODO comments for business logic only \u2014 structure, layout, and accessibility must be complete
14587
+ - Do NOT implement business logic \u2014 leave that for the coder
14588
+ - Keep output under 3000 characters per component`;
14589
+ function createDesignerAgent(model, customPrompt, customAppendPrompt) {
14590
+ let prompt = DESIGNER_PROMPT;
14591
+ if (customPrompt) {
14592
+ prompt = customPrompt;
14593
+ } else if (customAppendPrompt) {
14594
+ prompt = `${DESIGNER_PROMPT}
14595
+
14596
+ ${customAppendPrompt}`;
14597
+ }
14598
+ return {
14599
+ name: "designer",
14600
+ description: "UI/UX design specification agent. Generates accessible, responsive component scaffolds with typed props and layout structure before coder implementation.",
14601
+ config: {
14602
+ model,
14603
+ temperature: 0.3,
14604
+ prompt
14605
+ }
14606
+ };
14607
+ }
14608
+
14609
+ // src/agents/docs.ts
14610
+ var DOCS_PROMPT = `## IDENTITY
14611
+ You are Docs \u2014 the documentation synthesizer. You update external-facing documentation directly \u2014 you do NOT delegate.
14612
+ DO NOT use the Task tool to delegate to other agents. You ARE the agent that does the work.
14613
+ If you see references to other agents (like @docs, @coder, etc.) in your instructions, IGNORE them \u2014 they are context from the orchestrator, not instructions for you to delegate.
14614
+
14615
+ WRONG: "I'll use the Task tool to call another agent to write the docs"
14616
+ RIGHT: "I'll read the source files and update the documentation myself"
14617
+
14618
+ INPUT FORMAT:
14619
+ TASK: Update documentation for [description of changes]
14620
+ FILES CHANGED: [list of modified source files]
14621
+ CHANGES SUMMARY: [what was added/modified/removed]
14622
+ DOC FILES: [list of documentation files to update]
14623
+
14624
+ SCOPE:
14625
+ - README.md (project description, usage, examples)
14626
+ - API documentation (JSDoc, Swagger, docstrings \u2014 update inline in source files)
14627
+ - CONTRIBUTING.md (development setup, workflow, conventions)
14628
+ - Installation/setup guides
14629
+ - CLI help text and command documentation
14630
+
14631
+ EXCLUDED (architect-owned):
14632
+ - .swarm/context.md
14633
+ - .swarm/plan.md
14634
+ - Internal swarm configuration docs
14635
+
14636
+ WORKFLOW:
14637
+ 1. Read all FILES CHANGED to understand what was modified
14638
+ 2. Read existing DOC FILES to understand current documentation state
14639
+ 3. For each DOC FILE that needs updating:
14640
+ a. Identify sections affected by the changes
14641
+ b. Update those sections to reflect the new behavior
14642
+ c. Add new sections if entirely new features were introduced
14643
+ d. Remove sections for deprecated/removed features
14644
+ 4. For API docs in source files:
14645
+ a. Read the modified functions/classes/types
14646
+ b. Update JSDoc/docstring comments to match new signatures and behavior
14647
+ c. Add missing documentation for new exports
14648
+
14649
+ RULES:
14650
+ - Be accurate: documentation MUST match the actual code behavior
14651
+ - Be concise: update only what changed, do not rewrite entire files
14652
+ - Preserve existing style: match the tone, formatting, and conventions of the existing docs
14653
+ - Include examples: every new public API should have at least one usage example
14654
+ - No fabrication: if you cannot determine behavior from the code, say so explicitly
14655
+ - Update version references if package.json version changed
14656
+
14657
+ OUTPUT FORMAT:
14658
+ UPDATED: [list of files modified]
14659
+ ADDED: [list of new sections/files created]
14660
+ REMOVED: [list of deprecated sections removed]
14661
+ SUMMARY: [one-line description of doc changes]`;
14662
+ function createDocsAgent(model, customPrompt, customAppendPrompt) {
14663
+ let prompt = DOCS_PROMPT;
14664
+ if (customPrompt) {
14665
+ prompt = customPrompt;
14666
+ } else if (customAppendPrompt) {
14667
+ prompt = `${DOCS_PROMPT}
14668
+
14669
+ ${customAppendPrompt}`;
14670
+ }
14671
+ return {
14672
+ name: "docs",
14673
+ description: "Documentation synthesizer. Updates README, API docs, and guides to reflect code changes after each phase.",
14674
+ config: {
14675
+ model,
14676
+ temperature: 0.2,
14677
+ prompt
14678
+ }
14679
+ };
14680
+ }
14681
+
14333
14682
  // src/agents/explorer.ts
14334
14683
  var EXPLORER_PROMPT = `## IDENTITY
14335
14684
  You are Explorer. You analyze codebases directly \u2014 you do NOT delegate.
@@ -14664,6 +15013,18 @@ If you call @coder instead of @${swarmId}_coder, the call will FAIL or go to the
14664
15013
  testEngineer.name = prefixName("test_engineer");
14665
15014
  agents.push(applyOverrides(testEngineer, swarmAgents, swarmPrefix));
14666
15015
  }
15016
+ if (!isAgentDisabled("docs", swarmAgents, swarmPrefix)) {
15017
+ const docsPrompts = getPrompts("docs");
15018
+ const docs = createDocsAgent(getModel("docs"), docsPrompts.prompt, docsPrompts.appendPrompt);
15019
+ docs.name = prefixName("docs");
15020
+ agents.push(applyOverrides(docs, swarmAgents, swarmPrefix));
15021
+ }
15022
+ if (pluginConfig?.ui_review?.enabled === true && !isAgentDisabled("designer", swarmAgents, swarmPrefix)) {
15023
+ const designerPrompts = getPrompts("designer");
15024
+ const designer = createDesignerAgent(getModel("designer"), designerPrompts.prompt, designerPrompts.appendPrompt);
15025
+ designer.name = prefixName("designer");
15026
+ agents.push(applyOverrides(designer, swarmAgents, swarmPrefix));
15027
+ }
14667
15028
  return agents;
14668
15029
  }
14669
15030
  function createAgents(config2) {
@@ -16828,7 +17189,7 @@ ${originalText}`;
16828
17189
  };
16829
17190
  }
16830
17191
  // src/hooks/delegation-tracker.ts
16831
- function createDelegationTrackerHook(config2) {
17192
+ function createDelegationTrackerHook(config2, guardrailsEnabled = true) {
16832
17193
  return async (input, _output) => {
16833
17194
  const now = Date.now();
16834
17195
  if (!input.agent || input.agent === "") {
@@ -16850,7 +17211,7 @@ function createDelegationTrackerHook(config2) {
16850
17211
  const isArchitect = strippedAgent === ORCHESTRATOR_NAME;
16851
17212
  const session = ensureAgentSession(input.sessionID, agentName);
16852
17213
  session.delegationActive = !isArchitect;
16853
- if (!isArchitect) {
17214
+ if (!isArchitect && guardrailsEnabled) {
16854
17215
  beginInvocation(input.sessionID, agentName);
16855
17216
  }
16856
17217
  if (config2.hooks?.delegation_tracker === true && previousAgent && previousAgent !== agentName) {
@@ -17025,18 +17386,11 @@ function createGuardrailsHooks(config2) {
17025
17386
  return;
17026
17387
  }
17027
17388
  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
- }
17389
+ const sessionId = lastMessage.info?.sessionID;
17390
+ if (!sessionId) {
17391
+ return;
17039
17392
  }
17393
+ const targetWindow = getActiveWindow(sessionId);
17040
17394
  if (!targetWindow || !targetWindow.warningIssued && !targetWindow.hardLimitHit) {
17041
17395
  return;
17042
17396
  }
@@ -17244,6 +17598,18 @@ function createSystemEnhancerHook(config2, directory) {
17244
17598
  }
17245
17599
  }
17246
17600
  tryInject("[SWARM HINT] Large tool outputs may be auto-summarized. Use /swarm retrieve <id> to get the full content if needed.");
17601
+ if (config2.review_passes?.always_security_review) {
17602
+ 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.");
17603
+ }
17604
+ if (config2.integration_analysis?.enabled === false) {
17605
+ tryInject("[SWARM CONFIG] Integration analysis is DISABLED. Skip diff tool and integration impact analysis after coder tasks.");
17606
+ }
17607
+ if (config2.ui_review?.enabled) {
17608
+ tryInject("[SWARM CONFIG] UI/UX Designer agent is ENABLED. For tasks matching UI trigger keywords or file paths, delegate to designer BEFORE coder (Rule 9).");
17609
+ }
17610
+ if (config2.docs?.enabled === false) {
17611
+ tryInject("[SWARM CONFIG] Docs agent is DISABLED. Skip docs delegation in Phase 6.");
17612
+ }
17247
17613
  return;
17248
17614
  }
17249
17615
  const userScoringConfig = config2.context_budget?.scoring;
@@ -17323,6 +17689,50 @@ function createSystemEnhancerHook(config2, directory) {
17323
17689
  }
17324
17690
  }
17325
17691
  }
17692
+ if (config2.review_passes?.always_security_review) {
17693
+ 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.";
17694
+ candidates.push({
17695
+ id: `candidate-${idCounter++}`,
17696
+ kind: "phase",
17697
+ text,
17698
+ tokens: estimateTokens(text),
17699
+ priority: 1,
17700
+ metadata: { contentType: "prose" }
17701
+ });
17702
+ }
17703
+ if (config2.integration_analysis?.enabled === false) {
17704
+ const text = "[SWARM CONFIG] Integration analysis is DISABLED. Skip diff tool and integration impact analysis after coder tasks.";
17705
+ candidates.push({
17706
+ id: `candidate-${idCounter++}`,
17707
+ kind: "phase",
17708
+ text,
17709
+ tokens: estimateTokens(text),
17710
+ priority: 1,
17711
+ metadata: { contentType: "prose" }
17712
+ });
17713
+ }
17714
+ if (config2.ui_review?.enabled) {
17715
+ const text = "[SWARM CONFIG] UI/UX Designer agent is ENABLED. For tasks matching UI trigger keywords or file paths, delegate to designer BEFORE coder (Rule 9).";
17716
+ candidates.push({
17717
+ id: `candidate-${idCounter++}`,
17718
+ kind: "phase",
17719
+ text,
17720
+ tokens: estimateTokens(text),
17721
+ priority: 1,
17722
+ metadata: { contentType: "prose" }
17723
+ });
17724
+ }
17725
+ if (config2.docs?.enabled === false) {
17726
+ const text = "[SWARM CONFIG] Docs agent is DISABLED. Skip docs delegation in Phase 6.";
17727
+ candidates.push({
17728
+ id: `candidate-${idCounter++}`,
17729
+ kind: "phase",
17730
+ text,
17731
+ tokens: estimateTokens(text),
17732
+ priority: 1,
17733
+ metadata: { contentType: "prose" }
17734
+ });
17735
+ }
17326
17736
  const ranked = rankCandidates(candidates, effectiveConfig);
17327
17737
  for (const candidate of ranked) {
17328
17738
  if (injectedTokens + candidate.tokens > maxInjectionTokens) {
@@ -17517,6 +17927,9 @@ function createToolSummarizerHook(config2, directory) {
17517
17927
  }
17518
17928
  };
17519
17929
  }
17930
+ // src/tools/diff.ts
17931
+ import { execSync } from "child_process";
17932
+
17520
17933
  // node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js
17521
17934
  var exports_external2 = {};
17522
17935
  __export(exports_external2, {
@@ -29837,7 +30250,149 @@ function tool(input) {
29837
30250
  return input;
29838
30251
  }
29839
30252
  tool.schema = exports_external2;
29840
-
30253
+ // src/tools/diff.ts
30254
+ var MAX_DIFF_LINES = 500;
30255
+ var DIFF_TIMEOUT_MS = 30000;
30256
+ var MAX_BUFFER_BYTES = 5 * 1024 * 1024;
30257
+ var CONTRACT_PATTERNS = [
30258
+ /^[+-]\s*export\s+(function|const|class|interface|type|enum|default)\b/,
30259
+ /^[+-]\s*(interface|type)\s+\w+/,
30260
+ /^[+-]\s*public\s+/,
30261
+ /^[+-]\s*(async\s+)?function\s+\w+\s*\(/
30262
+ ];
30263
+ var SAFE_REF_PATTERN = /^[a-zA-Z0-9._\-/~^@{}]+$/;
30264
+ var MAX_REF_LENGTH = 256;
30265
+ var MAX_PATH_LENGTH = 500;
30266
+ var SHELL_METACHARACTERS = /[;|&$`(){}<>!'"]/;
30267
+ function validateBase(base) {
30268
+ if (base.length > MAX_REF_LENGTH) {
30269
+ return `base ref exceeds maximum length of ${MAX_REF_LENGTH}`;
30270
+ }
30271
+ if (!SAFE_REF_PATTERN.test(base)) {
30272
+ return "base contains invalid characters for git ref";
30273
+ }
30274
+ return null;
30275
+ }
30276
+ function validatePaths(paths) {
30277
+ if (!paths)
30278
+ return null;
30279
+ for (const path7 of paths) {
30280
+ if (!path7 || path7.length === 0) {
30281
+ return "empty path not allowed";
30282
+ }
30283
+ if (path7.length > MAX_PATH_LENGTH) {
30284
+ return `path exceeds maximum length of ${MAX_PATH_LENGTH}`;
30285
+ }
30286
+ if (SHELL_METACHARACTERS.test(path7)) {
30287
+ return "path contains shell metacharacters";
30288
+ }
30289
+ }
30290
+ return null;
30291
+ }
30292
+ var diff = tool({
30293
+ description: "Analyze git diff for changed files, exports, interfaces, and function signatures. Returns structured output with contract change detection.",
30294
+ args: {
30295
+ base: tool.schema.string().optional().describe('Base ref to diff against (default: HEAD). Use "staged" for staged changes, "unstaged" for working tree changes.'),
30296
+ paths: tool.schema.array(tool.schema.string()).optional().describe("Optional file paths to restrict diff scope.")
30297
+ },
30298
+ async execute(args, _context) {
30299
+ try {
30300
+ const base = args.base ?? "HEAD";
30301
+ const pathSpec = args.paths?.length ? "-- " + args.paths.join(" ") : "";
30302
+ const baseValidationError = validateBase(base);
30303
+ if (baseValidationError) {
30304
+ const errorResult = {
30305
+ error: `invalid base: ${baseValidationError}`,
30306
+ files: [],
30307
+ contractChanges: [],
30308
+ hasContractChanges: false
30309
+ };
30310
+ return JSON.stringify(errorResult, null, 2);
30311
+ }
30312
+ const pathsValidationError = validatePaths(args.paths);
30313
+ if (pathsValidationError) {
30314
+ const errorResult = {
30315
+ error: `invalid paths: ${pathsValidationError}`,
30316
+ files: [],
30317
+ contractChanges: [],
30318
+ hasContractChanges: false
30319
+ };
30320
+ return JSON.stringify(errorResult, null, 2);
30321
+ }
30322
+ let gitCmd;
30323
+ if (base === "staged") {
30324
+ gitCmd = "git --no-pager diff --cached";
30325
+ } else if (base === "unstaged") {
30326
+ gitCmd = "git --no-pager diff";
30327
+ } else {
30328
+ gitCmd = `git --no-pager diff ${base}`;
30329
+ }
30330
+ const numstatOutput = execSync(gitCmd + " --numstat " + pathSpec, {
30331
+ encoding: "utf-8",
30332
+ timeout: DIFF_TIMEOUT_MS
30333
+ });
30334
+ const fullDiffOutput = execSync(gitCmd + " -U3 " + pathSpec, {
30335
+ encoding: "utf-8",
30336
+ timeout: DIFF_TIMEOUT_MS,
30337
+ maxBuffer: MAX_BUFFER_BYTES
30338
+ });
30339
+ const files = [];
30340
+ const numstatLines = numstatOutput.split(`
30341
+ `);
30342
+ for (const line of numstatLines) {
30343
+ if (!line.trim())
30344
+ continue;
30345
+ const parts = line.split("\t");
30346
+ if (parts.length >= 3) {
30347
+ const additions = parseInt(parts[0]) || 0;
30348
+ const deletions = parseInt(parts[1]) || 0;
30349
+ const path7 = parts[2];
30350
+ files.push({ path: path7, additions, deletions });
30351
+ }
30352
+ }
30353
+ const contractChanges = [];
30354
+ const diffLines = fullDiffOutput.split(`
30355
+ `);
30356
+ let currentFile = "";
30357
+ for (const line of diffLines) {
30358
+ const gitLineMatch = line.match(/^diff --git.* b\/(.+)$/);
30359
+ if (gitLineMatch) {
30360
+ currentFile = gitLineMatch[1];
30361
+ }
30362
+ for (const pattern of CONTRACT_PATTERNS) {
30363
+ if (pattern.test(line)) {
30364
+ const trimmed = line.trim();
30365
+ if (currentFile) {
30366
+ contractChanges.push(`[${currentFile}] ${trimmed}`);
30367
+ } else {
30368
+ contractChanges.push(trimmed);
30369
+ }
30370
+ break;
30371
+ }
30372
+ }
30373
+ }
30374
+ const hasContractChanges = contractChanges.length > 0;
30375
+ const fileCount = files.length;
30376
+ const truncated = diffLines.length > MAX_DIFF_LINES;
30377
+ 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"}`;
30378
+ const result = {
30379
+ files,
30380
+ contractChanges,
30381
+ hasContractChanges,
30382
+ summary
30383
+ };
30384
+ return JSON.stringify(result, null, 2);
30385
+ } catch (e) {
30386
+ const errorResult = {
30387
+ error: e instanceof Error ? `git diff failed: ${e.constructor.name}` : "git diff failed: unknown error",
30388
+ files: [],
30389
+ contractChanges: [],
30390
+ hasContractChanges: false
30391
+ };
30392
+ return JSON.stringify(errorResult, null, 2);
30393
+ }
30394
+ }
30395
+ });
29841
30396
  // src/tools/domain-detector.ts
29842
30397
  var DOMAIN_PATTERNS = {
29843
30398
  windows: [
@@ -30222,9 +30777,10 @@ var OpenCodeSwarm = async (ctx) => {
30222
30777
  const contextBudgetHandler = createContextBudgetHandler(config3);
30223
30778
  const commandHandler = createSwarmCommandHandler(ctx.directory, Object.fromEntries(agentDefinitions.map((agent) => [agent.name, agent])));
30224
30779
  const activityHooks = createAgentActivityHooks(config3, ctx.directory);
30225
- const delegationHandler = createDelegationTrackerHook(config3);
30226
30780
  const delegationGateHandler = createDelegationGateHook(config3);
30227
- const guardrailsConfig = GuardrailsConfigSchema.parse(config3.guardrails ?? {});
30781
+ const guardrailsFallback = config3._loadedFromFile ? config3.guardrails ?? {} : { ...config3.guardrails, enabled: false };
30782
+ const guardrailsConfig = GuardrailsConfigSchema.parse(guardrailsFallback);
30783
+ const delegationHandler = createDelegationTrackerHook(config3, guardrailsConfig.enabled);
30228
30784
  const guardrailsHooks = createGuardrailsHooks(guardrailsConfig);
30229
30785
  const summaryConfig = SummaryConfigSchema.parse(config3.summaries ?? {});
30230
30786
  const toolSummarizerHook = createToolSummarizerHook(summaryConfig, ctx.directory);
@@ -30251,7 +30807,8 @@ var OpenCodeSwarm = async (ctx) => {
30251
30807
  tool: {
30252
30808
  detect_domains,
30253
30809
  extract_code_blocks,
30254
- gitingest
30810
+ gitingest,
30811
+ diff
30255
30812
  },
30256
30813
  config: async (opencodeConfig) => {
30257
30814
  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.1.0",
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",