opencode-swarm 6.13.0 → 6.13.2

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
@@ -310,6 +310,8 @@ Per-agent overrides:
310
310
  | build_check | Runs your project's native build/typecheck |
311
311
  | quality_budget | Enforces complexity, duplication, and test ratio limits |
312
312
  | pre_check_batch | Runs lint, secretscan, SAST, and quality budget in parallel (~15s vs ~60s sequential) |
313
+ | phase_complete | Enforces phase completion, verifies required agents, logs events, and resets state |
314
+
313
315
 
314
316
  All tools run locally. No Docker, no network calls, no external APIs.
315
317
 
@@ -586,11 +588,30 @@ The following tools can be assigned to agents via overrides:
586
588
  | `symbols` | Extract exported symbols |
587
589
  | `test_runner` | Run project tests |
588
590
  | `todo_extract` | Extract TODO/FIXME comments |
591
+ | `phase_complete` | Enforces phase completion, verifies required agents, logs events, resets state |
589
592
 
590
593
  ---
591
594
 
592
595
  ## Recent Changes
593
596
 
597
+ ### v6.13.2 — Pipeline Enforcement
598
+
599
+ This release adds enforcement-layer tooling and self-healing guardrails:
600
+
601
+ - **`phase_complete` tool**: Verifies all required agents were dispatched before a phase closes; emits events to `.swarm/events.jsonl`; configurable `enforce`/`warn` policy
602
+ - **Summarization loop fix**: `exempt_tools` config prevents `retrieve_summary` and `task` outputs from being re-summarized (fixes Issue #8)
603
+ - **Same-model adversarial detection**: Warns when coder and reviewer share the same model; `warn`/`gate`/`ignore` policy
604
+ - **Architect test guardrail (HF-1b)**: Prevents architect from running full `bun test` suite — must target specific files one at a time
605
+ - **Docs**: `docs/swarm-briefing.md` (LLM pipeline briefing), Task Field Reference in `docs/planning.md`
606
+
607
+ ### v6.13.1 — Consolidation & Defaults Fix
608
+
609
+ - **`consolidateSystemMessages`**: Merges multiple system messages into one at index 0
610
+ - **Test isolation helpers**: `createIsolatedTestEnv` and `assertSafeForWrite`
611
+ - **Coder self-verify guardrail (HF-1)**: Coder and test_engineer agents blocked from running build/test/lint
612
+ - **`/swarm` template fix**: `{{arguments}}` → `$ARGUMENTS`
613
+ - **DEFAULT_MODELS update**: `claude-sonnet-4-5` → `claude-sonnet-4-20250514`, `gemini-2.0-flash` → `gemini-2.5-flash`
614
+
594
615
  ### v6.13.0 — Context Efficiency
595
616
 
596
617
  This release focuses on reducing context usage and improving mode-conditional behavior:
@@ -650,6 +671,8 @@ Upcoming: v6.14 focuses on further context optimization and agent coordination i
650
671
  - [Design Rationale](docs/design-rationale.md)
651
672
  - [Installation Guide](docs/installation.md)
652
673
  - [Linux + Docker Desktop Install Guide](docs/installation-linux-docker.md)
674
+ - [Pre-Swarm Planning Guide](docs/planning.md)
675
+ - [Swarm Briefing for LLMs](docs/swarm-briefing.md)
653
676
 
654
677
  ---
655
678
 
package/dist/cli/index.js CHANGED
@@ -52,26 +52,14 @@ async function install() {
52
52
  console.log("\u2713 Disabled default OpenCode agents (explore, general)");
53
53
  if (!fs.existsSync(PLUGIN_CONFIG_PATH)) {
54
54
  const defaultConfig = {
55
- preset: "remote",
56
- presets: {
57
- remote: {
58
- architect: { model: "anthropic/claude-sonnet-4.5" },
59
- coder: { model: "openai/gpt-5.2-codex" },
60
- sme: { model: "google/gemini-3-flash" },
61
- reviewer: { model: "google/gemini-3-flash" },
62
- test_engineer: { model: "google/gemini-3-flash" }
63
- },
64
- hybrid: {
65
- architect: { model: "anthropic/claude-sonnet-4.5" },
66
- coder: { model: "ollama/qwen3:72b" },
67
- sme: { model: "npu/qwen3:14b" },
68
- reviewer: { model: "npu/qwen3:14b" },
69
- test_engineer: { model: "npu/qwen3:14b" }
70
- }
55
+ agents: {
56
+ architect: { model: "anthropic/claude-sonnet-4-20250514" },
57
+ coder: { model: "anthropic/claude-sonnet-4-20250514" },
58
+ sme: { model: "google/gemini-2.5-flash" },
59
+ reviewer: { model: "google/gemini-2.5-flash" },
60
+ test_engineer: { model: "google/gemini-2.5-flash" }
71
61
  },
72
- swarm_mode: "remote",
73
- max_iterations: 5,
74
- inject_phase_reminders: true
62
+ max_iterations: 5
75
63
  };
76
64
  saveJson(PLUGIN_CONFIG_PATH, defaultConfig);
77
65
  console.log("\u2713 Created default plugin config at:", PLUGIN_CONFIG_PATH);
@@ -5,5 +5,5 @@ export { ApprovalEvidenceSchema, BaseEvidenceSchema, DiffEvidenceSchema, EVIDENC
5
5
  export { loadAgentPrompt, loadPluginConfig, loadPluginConfigWithMeta, } from './loader';
6
6
  export type { MigrationStatus, Phase, PhaseStatus, Plan, Task, TaskSize, TaskStatus, } from './plan-schema';
7
7
  export { MigrationStatusSchema, PhaseSchema, PhaseStatusSchema, PlanSchema, TaskSchema, TaskSizeSchema, TaskStatusSchema, } from './plan-schema';
8
- export type { AgentOverrideConfig, AutomationCapabilities, AutomationConfig, AutomationMode, PipelineConfig, PluginConfig, SwarmConfig, } from './schema';
9
- export { AgentOverrideConfigSchema, AutomationCapabilitiesSchema, AutomationConfigSchema, AutomationModeSchema, PipelineConfigSchema, PluginConfigSchema, SwarmConfigSchema, } from './schema';
8
+ export type { AgentOverrideConfig, AutomationCapabilities, AutomationConfig, AutomationMode, PhaseCompleteConfig, PipelineConfig, PluginConfig, SwarmConfig, } from './schema';
9
+ export { AgentOverrideConfigSchema, AutomationCapabilitiesSchema, AutomationConfigSchema, AutomationModeSchema, PhaseCompleteConfigSchema, PipelineConfigSchema, PluginConfigSchema, SwarmConfigSchema, } from './schema';
@@ -190,12 +190,27 @@ export declare const PipelineConfigSchema: z.ZodObject<{
190
190
  parallel_precheck: z.ZodDefault<z.ZodBoolean>;
191
191
  }, z.core.$strip>;
192
192
  export type PipelineConfig = z.infer<typeof PipelineConfigSchema>;
193
+ export declare const PhaseCompleteConfigSchema: z.ZodObject<{
194
+ enabled: z.ZodDefault<z.ZodBoolean>;
195
+ required_agents: z.ZodDefault<z.ZodArray<z.ZodEnum<{
196
+ reviewer: "reviewer";
197
+ coder: "coder";
198
+ test_engineer: "test_engineer";
199
+ }>>>;
200
+ require_docs: z.ZodDefault<z.ZodBoolean>;
201
+ policy: z.ZodDefault<z.ZodEnum<{
202
+ enforce: "enforce";
203
+ warn: "warn";
204
+ }>>;
205
+ }, z.core.$strip>;
206
+ export type PhaseCompleteConfig = z.infer<typeof PhaseCompleteConfigSchema>;
193
207
  export declare const SummaryConfigSchema: z.ZodObject<{
194
208
  enabled: z.ZodDefault<z.ZodBoolean>;
195
209
  threshold_bytes: z.ZodDefault<z.ZodNumber>;
196
210
  max_summary_chars: z.ZodDefault<z.ZodNumber>;
197
211
  max_stored_bytes: z.ZodDefault<z.ZodNumber>;
198
212
  retention_days: z.ZodDefault<z.ZodNumber>;
213
+ exempt_tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
199
214
  }, z.core.$strip>;
200
215
  export type SummaryConfig = z.infer<typeof SummaryConfigSchema>;
201
216
  export declare const ReviewPassesConfigSchema: z.ZodObject<{
@@ -203,6 +218,16 @@ export declare const ReviewPassesConfigSchema: z.ZodObject<{
203
218
  security_globs: z.ZodDefault<z.ZodArray<z.ZodString>>;
204
219
  }, z.core.$strip>;
205
220
  export type ReviewPassesConfig = z.infer<typeof ReviewPassesConfigSchema>;
221
+ export declare const AdversarialDetectionConfigSchema: z.ZodObject<{
222
+ enabled: z.ZodDefault<z.ZodBoolean>;
223
+ policy: z.ZodDefault<z.ZodEnum<{
224
+ warn: "warn";
225
+ gate: "gate";
226
+ ignore: "ignore";
227
+ }>>;
228
+ pairs: z.ZodDefault<z.ZodArray<z.ZodTuple<[z.ZodString, z.ZodString], null>>>;
229
+ }, z.core.$strip>;
230
+ export type AdversarialDetectionConfig = z.infer<typeof AdversarialDetectionConfigSchema>;
206
231
  export declare const IntegrationAnalysisConfigSchema: z.ZodObject<{
207
232
  enabled: z.ZodDefault<z.ZodBoolean>;
208
233
  }, z.core.$strip>;
@@ -367,6 +392,19 @@ export declare const PluginConfigSchema: z.ZodObject<{
367
392
  pipeline: z.ZodOptional<z.ZodObject<{
368
393
  parallel_precheck: z.ZodDefault<z.ZodBoolean>;
369
394
  }, z.core.$strip>>;
395
+ phase_complete: z.ZodOptional<z.ZodObject<{
396
+ enabled: z.ZodDefault<z.ZodBoolean>;
397
+ required_agents: z.ZodDefault<z.ZodArray<z.ZodEnum<{
398
+ reviewer: "reviewer";
399
+ coder: "coder";
400
+ test_engineer: "test_engineer";
401
+ }>>>;
402
+ require_docs: z.ZodDefault<z.ZodBoolean>;
403
+ policy: z.ZodDefault<z.ZodEnum<{
404
+ enforce: "enforce";
405
+ warn: "warn";
406
+ }>>;
407
+ }, z.core.$strip>>;
370
408
  qa_retry_limit: z.ZodDefault<z.ZodNumber>;
371
409
  inject_phase_reminders: z.ZodDefault<z.ZodBoolean>;
372
410
  hooks: z.ZodOptional<z.ZodObject<{
@@ -479,11 +517,21 @@ export declare const PluginConfigSchema: z.ZodObject<{
479
517
  max_summary_chars: z.ZodDefault<z.ZodNumber>;
480
518
  max_stored_bytes: z.ZodDefault<z.ZodNumber>;
481
519
  retention_days: z.ZodDefault<z.ZodNumber>;
520
+ exempt_tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
482
521
  }, z.core.$strip>>;
483
522
  review_passes: z.ZodOptional<z.ZodObject<{
484
523
  always_security_review: z.ZodDefault<z.ZodBoolean>;
485
524
  security_globs: z.ZodDefault<z.ZodArray<z.ZodString>>;
486
525
  }, z.core.$strip>>;
526
+ adversarial_detection: z.ZodOptional<z.ZodObject<{
527
+ enabled: z.ZodDefault<z.ZodBoolean>;
528
+ policy: z.ZodDefault<z.ZodEnum<{
529
+ warn: "warn";
530
+ gate: "gate";
531
+ ignore: "ignore";
532
+ }>>;
533
+ pairs: z.ZodDefault<z.ZodArray<z.ZodTuple<[z.ZodString, z.ZodString], null>>>;
534
+ }, z.core.$strip>>;
487
535
  integration_analysis: z.ZodOptional<z.ZodObject<{
488
536
  enabled: z.ZodDefault<z.ZodBoolean>;
489
537
  }, z.core.$strip>>;
@@ -0,0 +1,15 @@
1
+ import type { PluginConfig } from '../config';
2
+ /**
3
+ * Resolve the model for a given agent by checking config overrides,
4
+ * swarm configurations, and falling back to defaults.
5
+ */
6
+ export declare function resolveAgentModel(agentName: string, config: PluginConfig): string;
7
+ /**
8
+ * Detect if two agents share the same model (adversarial pair).
9
+ * Returns the shared model string if matched, null otherwise.
10
+ */
11
+ export declare function detectAdversarialPair(agentA: string, agentB: string, config: PluginConfig): string | null;
12
+ /**
13
+ * Format an adversarial warning message based on policy.
14
+ */
15
+ export declare function formatAdversarialWarning(agentA: string, agentB: string, sharedModel: string, policy: string): string;
package/dist/index.js CHANGED
@@ -31449,7 +31449,8 @@ var TOOL_NAMES = [
31449
31449
  "detect_domains",
31450
31450
  "gitingest",
31451
31451
  "retrieve_summary",
31452
- "extract_code_blocks"
31452
+ "extract_code_blocks",
31453
+ "phase_complete"
31453
31454
  ];
31454
31455
  var TOOL_NAME_SET = new Set(TOOL_NAMES);
31455
31456
 
@@ -31565,16 +31566,16 @@ for (const [agentName, tools] of Object.entries(AGENT_TOOL_MAP)) {
31565
31566
  }
31566
31567
  }
31567
31568
  var DEFAULT_MODELS = {
31568
- architect: "anthropic/claude-sonnet-4-5",
31569
- explorer: "google/gemini-2.0-flash",
31570
- coder: "anthropic/claude-sonnet-4-5",
31571
- test_engineer: "google/gemini-2.0-flash",
31572
- sme: "google/gemini-2.0-flash",
31573
- reviewer: "google/gemini-2.0-flash",
31574
- critic: "google/gemini-2.0-flash",
31575
- docs: "google/gemini-2.0-flash",
31576
- designer: "google/gemini-2.0-flash",
31577
- default: "google/gemini-2.0-flash"
31569
+ architect: "anthropic/claude-sonnet-4-20250514",
31570
+ explorer: "google/gemini-2.5-flash",
31571
+ coder: "anthropic/claude-sonnet-4-20250514",
31572
+ test_engineer: "google/gemini-2.5-flash",
31573
+ sme: "google/gemini-2.5-flash",
31574
+ reviewer: "google/gemini-2.5-flash",
31575
+ critic: "google/gemini-2.5-flash",
31576
+ docs: "google/gemini-2.5-flash",
31577
+ designer: "google/gemini-2.5-flash",
31578
+ default: "google/gemini-2.5-flash"
31578
31579
  };
31579
31580
  var DEFAULT_SCORING_CONFIG = {
31580
31581
  enabled: false,
@@ -31785,12 +31786,19 @@ var GateConfigSchema = exports_external.object({
31785
31786
  var PipelineConfigSchema = exports_external.object({
31786
31787
  parallel_precheck: exports_external.boolean().default(true)
31787
31788
  });
31789
+ var PhaseCompleteConfigSchema = exports_external.object({
31790
+ enabled: exports_external.boolean().default(true),
31791
+ required_agents: exports_external.array(exports_external.enum(["coder", "reviewer", "test_engineer"])).default(["coder", "reviewer", "test_engineer"]),
31792
+ require_docs: exports_external.boolean().default(true),
31793
+ policy: exports_external.enum(["enforce", "warn"]).default("enforce")
31794
+ });
31788
31795
  var SummaryConfigSchema = exports_external.object({
31789
31796
  enabled: exports_external.boolean().default(true),
31790
31797
  threshold_bytes: exports_external.number().min(1024).max(1048576).default(20480),
31791
31798
  max_summary_chars: exports_external.number().min(100).max(5000).default(1000),
31792
31799
  max_stored_bytes: exports_external.number().min(10240).max(104857600).default(10485760),
31793
- retention_days: exports_external.number().min(1).max(365).default(7)
31800
+ retention_days: exports_external.number().min(1).max(365).default(7),
31801
+ exempt_tools: exports_external.array(exports_external.string()).default(["retrieve_summary", "task"])
31794
31802
  });
31795
31803
  var ReviewPassesConfigSchema = exports_external.object({
31796
31804
  always_security_review: exports_external.boolean().default(false),
@@ -31804,6 +31812,11 @@ var ReviewPassesConfigSchema = exports_external.object({
31804
31812
  "**/token/**"
31805
31813
  ])
31806
31814
  });
31815
+ var AdversarialDetectionConfigSchema = exports_external.object({
31816
+ enabled: exports_external.boolean().default(true),
31817
+ policy: exports_external.enum(["warn", "gate", "ignore"]).default("warn"),
31818
+ pairs: exports_external.array(exports_external.tuple([exports_external.string(), exports_external.string()])).default([["coder", "reviewer"]])
31819
+ });
31807
31820
  var IntegrationAnalysisConfigSchema = exports_external.object({
31808
31821
  enabled: exports_external.boolean().default(true)
31809
31822
  });
@@ -32035,6 +32048,7 @@ var PluginConfigSchema = exports_external.object({
32035
32048
  swarms: exports_external.record(exports_external.string(), SwarmConfigSchema).optional(),
32036
32049
  max_iterations: exports_external.number().min(1).max(10).default(5),
32037
32050
  pipeline: PipelineConfigSchema.optional(),
32051
+ phase_complete: PhaseCompleteConfigSchema.optional(),
32038
32052
  qa_retry_limit: exports_external.number().min(1).max(10).default(3),
32039
32053
  inject_phase_reminders: exports_external.boolean().default(true),
32040
32054
  hooks: HooksConfigSchema.optional(),
@@ -32046,6 +32060,7 @@ var PluginConfigSchema = exports_external.object({
32046
32060
  evidence: EvidenceConfigSchema.optional(),
32047
32061
  summaries: SummaryConfigSchema.optional(),
32048
32062
  review_passes: ReviewPassesConfigSchema.optional(),
32063
+ adversarial_detection: AdversarialDetectionConfigSchema.optional(),
32049
32064
  integration_analysis: IntegrationAnalysisConfigSchema.optional(),
32050
32065
  docs: DocsConfigSchema.optional(),
32051
32066
  ui_review: UIReviewConfigSchema.optional(),
@@ -32108,6 +32123,22 @@ function loadRawConfigFromPath(configPath) {
32108
32123
  return { config: null, fileExisted: false, hadError: false };
32109
32124
  }
32110
32125
  }
32126
+ function migratePresetsConfig(raw) {
32127
+ if (raw.presets && typeof raw.presets === "object" && !raw.agents) {
32128
+ const presetName = raw.preset || "remote";
32129
+ const presets = raw.presets;
32130
+ const activePreset = presets[presetName] || Object.values(presets)[0];
32131
+ if (activePreset && typeof activePreset === "object") {
32132
+ const migrated = { ...raw, agents: activePreset };
32133
+ delete migrated.preset;
32134
+ delete migrated.presets;
32135
+ delete migrated.swarm_mode;
32136
+ console.warn("[opencode-swarm] Migrated v6.12 presets config to agents format. Consider updating your opencode-swarm.json.");
32137
+ return migrated;
32138
+ }
32139
+ }
32140
+ return raw;
32141
+ }
32111
32142
  function loadPluginConfig(directory) {
32112
32143
  const userConfigPath = path.join(getUserConfigDir(), "opencode", CONFIG_FILENAME);
32113
32144
  const projectConfigPath = path.join(directory, ".opencode", CONFIG_FILENAME);
@@ -32121,6 +32152,7 @@ function loadPluginConfig(directory) {
32121
32152
  if (rawProjectConfig) {
32122
32153
  mergedRaw = deepMerge(mergedRaw, rawProjectConfig);
32123
32154
  }
32155
+ mergedRaw = migratePresetsConfig(mergedRaw);
32124
32156
  const result = PluginConfigSchema.safeParse(mergedRaw);
32125
32157
  if (!result.success) {
32126
32158
  if (rawUserConfig) {
@@ -34434,7 +34466,10 @@ function startAgentSession(sessionId, agentName, staleDurationMs = 7200000) {
34434
34466
  lastGateFailure: null,
34435
34467
  partialGateWarningIssued: false,
34436
34468
  selfFixAttempted: false,
34437
- catastrophicPhaseWarnings: new Set
34469
+ catastrophicPhaseWarnings: new Set,
34470
+ lastPhaseCompleteTimestamp: 0,
34471
+ lastPhaseCompletePhase: 0,
34472
+ phaseAgentsDispatched: new Set
34438
34473
  };
34439
34474
  swarmState.agentSessions.set(sessionId, sessionState);
34440
34475
  swarmState.activeAgent.set(sessionId, agentName);
@@ -34485,6 +34520,15 @@ function ensureAgentSession(sessionId, agentName) {
34485
34520
  if (!session.catastrophicPhaseWarnings) {
34486
34521
  session.catastrophicPhaseWarnings = new Set;
34487
34522
  }
34523
+ if (session.lastPhaseCompleteTimestamp === undefined) {
34524
+ session.lastPhaseCompleteTimestamp = 0;
34525
+ }
34526
+ if (session.lastPhaseCompletePhase === undefined) {
34527
+ session.lastPhaseCompletePhase = 0;
34528
+ }
34529
+ if (!session.phaseAgentsDispatched) {
34530
+ session.phaseAgentsDispatched = new Set;
34531
+ }
34488
34532
  session.lastToolCallTime = now;
34489
34533
  return session;
34490
34534
  }
@@ -34553,6 +34597,17 @@ function pruneOldWindows(sessionId, maxAgeMs = 24 * 60 * 60 * 1000, maxWindows =
34553
34597
  const toKeep = sorted.slice(0, maxWindows);
34554
34598
  session.windows = Object.fromEntries(toKeep);
34555
34599
  }
34600
+ function recordPhaseAgentDispatch(sessionId, agentName) {
34601
+ const session = swarmState.agentSessions.get(sessionId);
34602
+ if (!session) {
34603
+ return;
34604
+ }
34605
+ if (!session.phaseAgentsDispatched) {
34606
+ session.phaseAgentsDispatched = new Set;
34607
+ }
34608
+ const normalizedName = stripKnownSwarmPrefix(agentName);
34609
+ session.phaseAgentsDispatched.add(normalizedName);
34610
+ }
34556
34611
 
34557
34612
  // src/commands/benchmark.ts
34558
34613
  init_utils();
@@ -36663,6 +36718,7 @@ function createDelegationTrackerHook(config3, guardrailsEnabled = true) {
36663
36718
  const isArchitect = strippedAgent === ORCHESTRATOR_NAME;
36664
36719
  const session = ensureAgentSession(input.sessionID, agentName);
36665
36720
  session.delegationActive = !isArchitect;
36721
+ recordPhaseAgentDispatch(input.sessionID, agentName);
36666
36722
  if (!isArchitect && guardrailsEnabled) {
36667
36723
  beginInvocation(input.sessionID, agentName);
36668
36724
  }
@@ -37114,8 +37170,8 @@ function hashArgs(args2) {
37114
37170
  // src/hooks/messages-transform.ts
37115
37171
  function consolidateSystemMessages(messages) {
37116
37172
  if (messages.length > 0 && messages[0].role === "system" && messages[0].content !== undefined && typeof messages[0].content === "string" && messages[0].content.trim().length > 0) {
37117
- const systemMessageCount = messages.filter((m) => m.role === "system" && typeof m.content === "string" && m.content.trim().length > 0 && m.tool_call_id === undefined && m.name === undefined).length;
37118
- if (systemMessageCount === 1) {
37173
+ const totalSystemCount = messages.filter((m) => m.role === "system").length;
37174
+ if (totalSystemCount === 1) {
37119
37175
  return [...messages];
37120
37176
  }
37121
37177
  }
@@ -37123,24 +37179,32 @@ function consolidateSystemMessages(messages) {
37123
37179
  const systemContents = [];
37124
37180
  for (let i2 = 0;i2 < messages.length; i2++) {
37125
37181
  const message = messages[i2];
37126
- if (message.role !== "system") {
37182
+ if (message.role !== "system")
37127
37183
  continue;
37128
- }
37129
- if (message.tool_call_id !== undefined || message.name !== undefined) {
37130
- continue;
37131
- }
37132
- if (typeof message.content !== "string") {
37133
- continue;
37134
- }
37135
- const trimmedContent = message.content.trim();
37136
- if (trimmedContent.length === 0) {
37184
+ if (message.tool_call_id !== undefined || message.name !== undefined)
37137
37185
  continue;
37186
+ let textContent = null;
37187
+ if (typeof message.content === "string") {
37188
+ const trimmed = message.content.trim();
37189
+ if (trimmed.length > 0)
37190
+ textContent = trimmed;
37191
+ } else if (Array.isArray(message.content)) {
37192
+ const texts = message.content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text.trim()).filter((t) => t.length > 0);
37193
+ if (texts.length > 0)
37194
+ textContent = texts.join(`
37195
+ `);
37138
37196
  }
37139
37197
  systemMessageIndices.push(i2);
37140
- systemContents.push(trimmedContent);
37198
+ if (textContent) {
37199
+ systemContents.push(textContent);
37200
+ }
37141
37201
  }
37142
37202
  if (systemContents.length === 0) {
37143
- return [...messages];
37203
+ return messages.filter((m, idx) => {
37204
+ if (m.role !== "system")
37205
+ return true;
37206
+ return idx === 0;
37207
+ });
37144
37208
  }
37145
37209
  const mergedSystemContent = systemContents.join(`
37146
37210
 
@@ -37150,14 +37214,23 @@ function consolidateSystemMessages(messages) {
37150
37214
  result.push({
37151
37215
  role: "system",
37152
37216
  content: mergedSystemContent,
37153
- ...Object.fromEntries(Object.entries(firstSystemMessage).filter(([key]) => key !== "role" && key !== "content"))
37217
+ ...Object.fromEntries(Object.entries(firstSystemMessage).filter(([key]) => key !== "role" && key !== "content" && key !== "name" && key !== "tool_call_id"))
37154
37218
  });
37155
37219
  for (let i2 = 0;i2 < messages.length; i2++) {
37156
- if (!systemMessageIndices.includes(i2)) {
37157
- result.push({ ...messages[i2] });
37220
+ const message = messages[i2];
37221
+ if (systemMessageIndices.includes(i2)) {
37222
+ continue;
37158
37223
  }
37224
+ if (message.role === "system" && typeof message.content === "string" && message.content.trim().length === 0) {
37225
+ continue;
37226
+ }
37227
+ result.push({ ...message });
37159
37228
  }
37160
- return result;
37229
+ return result.filter((msg, idx) => {
37230
+ if (idx === 0)
37231
+ return true;
37232
+ return msg.role !== "system";
37233
+ });
37161
37234
  }
37162
37235
  // src/hooks/phase-monitor.ts
37163
37236
  init_manager2();
@@ -37552,6 +37625,39 @@ init_preflight_service();
37552
37625
  // src/hooks/system-enhancer.ts
37553
37626
  init_utils();
37554
37627
 
37628
+ // src/hooks/adversarial-detector.ts
37629
+ function safeGet(obj, key) {
37630
+ if (!obj || !Object.hasOwn(obj, key))
37631
+ return;
37632
+ return obj[key];
37633
+ }
37634
+ function resolveAgentModel(agentName, config3) {
37635
+ const baseName = stripKnownSwarmPrefix(agentName).toLowerCase();
37636
+ const agentOverride = safeGet(config3.agents, baseName)?.model;
37637
+ if (agentOverride)
37638
+ return agentOverride;
37639
+ if (config3.swarms) {
37640
+ for (const swarm of Object.values(config3.swarms)) {
37641
+ const swarmModel = safeGet(swarm.agents, baseName)?.model;
37642
+ if (swarmModel)
37643
+ return swarmModel;
37644
+ }
37645
+ }
37646
+ const defaultModel = safeGet(DEFAULT_MODELS, baseName);
37647
+ return defaultModel ?? DEFAULT_MODELS.default;
37648
+ }
37649
+ function detectAdversarialPair(agentA, agentB, config3) {
37650
+ const modelA = resolveAgentModel(agentA, config3).toLowerCase();
37651
+ const modelB = resolveAgentModel(agentB, config3).toLowerCase();
37652
+ return modelA === modelB ? modelA : null;
37653
+ }
37654
+ function formatAdversarialWarning(agentA, agentB, sharedModel, policy) {
37655
+ if (policy === "gate") {
37656
+ return `\u26A0\uFE0F GATE POLICY: Same-model adversarial pair detected. Agent ${agentA} and checker ${agentB} both use model ${sharedModel}. This requires extra scrutiny \u2014 escalate if issues are found.`;
37657
+ }
37658
+ return `\u26A0\uFE0F Same-model adversarial pair detected. Agent ${agentA} and checker ${agentB} both use model ${sharedModel}. Review may lack independence.`;
37659
+ }
37660
+
37555
37661
  // src/hooks/context-scoring.ts
37556
37662
  function calculateAgeFactor(ageHours, config3) {
37557
37663
  if (ageHours <= 0) {
@@ -37694,6 +37800,35 @@ function createSystemEnhancerHook(config3, directory) {
37694
37800
  if (config3.secretscan?.enabled === false) {
37695
37801
  tryInject("[SWARM CONFIG] Secretscan gate is DISABLED. Skip secretscan in QA sequence.");
37696
37802
  }
37803
+ const activeAgent_hf1 = swarmState.activeAgent.get(_input.sessionID ?? "");
37804
+ const baseRole = activeAgent_hf1 ? stripKnownSwarmPrefix(activeAgent_hf1) : null;
37805
+ if (baseRole === "coder" || baseRole === "test_engineer") {
37806
+ tryInject("[SWARM CONFIG] You must NOT run build, test, lint, or type-check commands (npm run build, bun test, npx tsc, eslint, etc.). Make ONLY the code changes specified in your task. Verification is handled by the reviewer agent \u2014 do not self-verify. If your task explicitly asks you to run a specific command, that is the only exception.");
37807
+ }
37808
+ if (baseRole === "architect" || baseRole === null) {
37809
+ tryInject("[SWARM CONFIG] You must NEVER run the full test suite or batch test files. If you need to verify changes, run ONLY the specific test files for code YOU modified in this session \u2014 one file at a time, strictly serial. Do not run tests from directories or files unrelated to your changes. Do not run bun test without an explicit file path. When possible, delegate test execution to the test_engineer agent instead of running tests yourself.");
37810
+ }
37811
+ if (config3.adversarial_detection?.enabled !== false) {
37812
+ const activeAgent_adv = swarmState.activeAgent.get(_input.sessionID ?? "");
37813
+ if (activeAgent_adv) {
37814
+ const baseRole_adv = stripKnownSwarmPrefix(activeAgent_adv);
37815
+ const pairs_adv = config3.adversarial_detection?.pairs ?? [
37816
+ ["coder", "reviewer"]
37817
+ ];
37818
+ const policy_adv = config3.adversarial_detection?.policy ?? "warn";
37819
+ for (const [agentA, agentB] of pairs_adv) {
37820
+ if (baseRole_adv === agentB) {
37821
+ const sharedModel = detectAdversarialPair(agentA, agentB, config3);
37822
+ if (sharedModel) {
37823
+ const warningText = formatAdversarialWarning(agentA, agentB, sharedModel, policy_adv);
37824
+ if (policy_adv !== "ignore") {
37825
+ tryInject(`[SWARM CONFIG] ${warningText}`);
37826
+ }
37827
+ }
37828
+ }
37829
+ }
37830
+ }
37831
+ }
37697
37832
  if (mode !== "DISCOVER") {
37698
37833
  const sessionId_preflight = _input.sessionID;
37699
37834
  const activeAgent_preflight = swarmState.activeAgent.get(sessionId_preflight ?? "");
@@ -37955,6 +38090,34 @@ function createSystemEnhancerHook(config3, directory) {
37955
38090
  metadata: { contentType: "prose" }
37956
38091
  });
37957
38092
  }
38093
+ if (config3.adversarial_detection?.enabled !== false) {
38094
+ const activeAgent_adv_b = swarmState.activeAgent.get(_input.sessionID ?? "");
38095
+ if (activeAgent_adv_b) {
38096
+ const baseRole_adv_b = stripKnownSwarmPrefix(activeAgent_adv_b);
38097
+ const pairs_adv_b = config3.adversarial_detection?.pairs ?? [
38098
+ ["coder", "reviewer"]
38099
+ ];
38100
+ const policy_adv_b = config3.adversarial_detection?.policy ?? "warn";
38101
+ for (const [agentA_b, agentB_b] of pairs_adv_b) {
38102
+ if (baseRole_adv_b === agentB_b) {
38103
+ const sharedModel_b = detectAdversarialPair(agentA_b, agentB_b, config3);
38104
+ if (sharedModel_b) {
38105
+ const warningText_b = formatAdversarialWarning(agentA_b, agentB_b, sharedModel_b, policy_adv_b);
38106
+ if (policy_adv_b !== "ignore") {
38107
+ candidates.push({
38108
+ id: `candidate-${idCounter++}`,
38109
+ kind: "agent_context",
38110
+ text: `[SWARM CONFIG] ${warningText_b}`,
38111
+ tokens: estimateTokens(warningText_b),
38112
+ priority: 2,
38113
+ metadata: { contentType: "prose" }
38114
+ });
38115
+ }
38116
+ }
38117
+ }
38118
+ }
38119
+ }
38120
+ }
37958
38121
  const sessionId_preflight_b = _input.sessionID;
37959
38122
  const activeAgent_preflight_b = swarmState.activeAgent.get(sessionId_preflight_b ?? "");
37960
38123
  const isArchitectForPreflight_b = !activeAgent_preflight_b || stripKnownSwarmPrefix(activeAgent_preflight_b) === "architect";
@@ -38273,6 +38436,10 @@ function createToolSummarizerHook(config3, directory) {
38273
38436
  if (typeof output.output !== "string" || output.output.length === 0) {
38274
38437
  return;
38275
38438
  }
38439
+ const exemptTools = config3.exempt_tools ?? ["retrieve_summary", "task"];
38440
+ if (exemptTools.includes(input.tool)) {
38441
+ return;
38442
+ }
38276
38443
  if (!shouldSummarize(output.output, config3.threshold_bytes)) {
38277
38444
  return;
38278
38445
  }
@@ -40385,9 +40552,172 @@ var imports = tool({
40385
40552
  // src/tools/index.ts
40386
40553
  init_lint();
40387
40554
 
40555
+ // src/tools/phase-complete.ts
40556
+ init_tool();
40557
+ import * as fs18 from "fs";
40558
+ init_utils2();
40559
+ function getDelegationsSince(sessionID, sinceTimestamp) {
40560
+ const chain = swarmState.delegationChains.get(sessionID);
40561
+ if (!chain) {
40562
+ return [];
40563
+ }
40564
+ if (sinceTimestamp === 0) {
40565
+ return chain;
40566
+ }
40567
+ return chain.filter((entry) => entry.timestamp > sinceTimestamp);
40568
+ }
40569
+ function normalizeAgentsFromDelegations(delegations) {
40570
+ const agents = new Set;
40571
+ for (const delegation of delegations) {
40572
+ const normalizedFrom = stripKnownSwarmPrefix(delegation.from);
40573
+ const normalizedTo = stripKnownSwarmPrefix(delegation.to);
40574
+ agents.add(normalizedFrom);
40575
+ agents.add(normalizedTo);
40576
+ }
40577
+ return agents;
40578
+ }
40579
+ async function executePhaseComplete(args2) {
40580
+ const phase = Number(args2.phase);
40581
+ const summary = args2.summary;
40582
+ const sessionID = args2.sessionID;
40583
+ if (Number.isNaN(phase) || phase < 1) {
40584
+ return JSON.stringify({
40585
+ success: false,
40586
+ phase,
40587
+ message: "Invalid phase number",
40588
+ agentsDispatched: [],
40589
+ warnings: ["Phase must be a positive number"]
40590
+ }, null, 2);
40591
+ }
40592
+ if (!sessionID) {
40593
+ return JSON.stringify({
40594
+ success: false,
40595
+ phase,
40596
+ message: "Session ID is required",
40597
+ agentsDispatched: [],
40598
+ warnings: [
40599
+ "sessionID parameter is required for phase completion tracking"
40600
+ ]
40601
+ }, null, 2);
40602
+ }
40603
+ const session = ensureAgentSession(sessionID);
40604
+ const lastCompletionTimestamp = session.lastPhaseCompleteTimestamp ?? 0;
40605
+ const recentDelegations = getDelegationsSince(sessionID, lastCompletionTimestamp);
40606
+ const delegationAgents = normalizeAgentsFromDelegations(recentDelegations);
40607
+ const trackedAgents = session.phaseAgentsDispatched ?? new Set;
40608
+ const allAgents = new Set([...delegationAgents, ...trackedAgents]);
40609
+ const agentsDispatched = Array.from(allAgents).sort();
40610
+ const directory = process.cwd();
40611
+ const { config: config3 } = loadPluginConfigWithMeta(directory);
40612
+ let phaseCompleteConfig;
40613
+ try {
40614
+ phaseCompleteConfig = PhaseCompleteConfigSchema.parse(config3.phase_complete ?? {});
40615
+ } catch (parseError) {
40616
+ return JSON.stringify({
40617
+ success: false,
40618
+ phase,
40619
+ status: "incomplete",
40620
+ message: `Invalid phase_complete configuration: ${parseError instanceof Error ? parseError.message : "Unknown error"}`,
40621
+ agentsDispatched,
40622
+ agentsMissing: [],
40623
+ warnings: ["Configuration validation failed"]
40624
+ }, null, 2);
40625
+ }
40626
+ if (phaseCompleteConfig.enabled === false) {
40627
+ return JSON.stringify({
40628
+ success: true,
40629
+ phase,
40630
+ status: "disabled",
40631
+ message: `Phase ${phase} complete (enforcement disabled)`,
40632
+ agentsDispatched,
40633
+ agentsMissing: [],
40634
+ warnings: []
40635
+ }, null, 2);
40636
+ }
40637
+ const effectiveRequired = [...phaseCompleteConfig.required_agents];
40638
+ if (phaseCompleteConfig.require_docs && !effectiveRequired.includes("docs")) {
40639
+ effectiveRequired.push("docs");
40640
+ }
40641
+ const agentsMissing = effectiveRequired.filter((req) => !allAgents.has(req));
40642
+ const warnings = [];
40643
+ let success3 = true;
40644
+ let status = "success";
40645
+ const safeSummary = summary?.trim().slice(0, 500);
40646
+ let message = safeSummary ? `Phase ${phase} completed: ${safeSummary}` : `Phase ${phase} completed`;
40647
+ if (agentsMissing.length > 0) {
40648
+ if (phaseCompleteConfig.policy === "enforce") {
40649
+ success3 = false;
40650
+ status = "incomplete";
40651
+ message = `Phase ${phase} incomplete: missing required agents: ${agentsMissing.join(", ")}`;
40652
+ } else {
40653
+ status = "warned";
40654
+ warnings.push(`Warning: phase ${phase} missing required agents: ${agentsMissing.join(", ")}`);
40655
+ }
40656
+ }
40657
+ const now = Date.now();
40658
+ const durationMs = now - lastCompletionTimestamp;
40659
+ const event = {
40660
+ event: "phase_complete",
40661
+ phase,
40662
+ timestamp: new Date(now).toISOString(),
40663
+ agents_dispatched: agentsDispatched,
40664
+ agents_missing: agentsMissing,
40665
+ status,
40666
+ summary: safeSummary ?? null
40667
+ };
40668
+ try {
40669
+ const eventsPath = validateSwarmPath(directory, "events.jsonl");
40670
+ fs18.appendFileSync(eventsPath, `${JSON.stringify(event)}
40671
+ `, "utf-8");
40672
+ } catch (writeError) {
40673
+ warnings.push(`Warning: failed to write phase complete event: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
40674
+ }
40675
+ if (success3) {
40676
+ session.phaseAgentsDispatched = new Set;
40677
+ session.lastPhaseCompleteTimestamp = now;
40678
+ session.lastPhaseCompletePhase = phase;
40679
+ }
40680
+ const result = {
40681
+ success: success3,
40682
+ phase,
40683
+ status,
40684
+ message,
40685
+ agentsDispatched,
40686
+ agentsMissing,
40687
+ warnings
40688
+ };
40689
+ return JSON.stringify({ ...result, timestamp: event.timestamp, duration_ms: durationMs }, null, 2);
40690
+ }
40691
+ var phase_complete = tool({
40692
+ description: "Mark a phase as complete and track which agents were dispatched. " + "Used for phase completion gating and tracking. " + "Accepts phase number and optional summary. Returns list of agents that were dispatched.",
40693
+ args: {
40694
+ phase: tool.schema.number().describe("The phase number being completed (e.g., 1, 2, 3)"),
40695
+ summary: tool.schema.string().optional().describe("Optional summary of what was accomplished in this phase"),
40696
+ sessionID: tool.schema.string().optional().describe("Session ID for tracking state (auto-provided by plugin context)")
40697
+ },
40698
+ execute: async (args2) => {
40699
+ let phaseCompleteArgs;
40700
+ try {
40701
+ phaseCompleteArgs = {
40702
+ phase: Number(args2.phase),
40703
+ summary: args2.summary !== undefined ? String(args2.summary) : undefined,
40704
+ sessionID: args2.sessionID !== undefined ? String(args2.sessionID) : undefined
40705
+ };
40706
+ } catch {
40707
+ return JSON.stringify({
40708
+ success: false,
40709
+ phase: 0,
40710
+ message: "Invalid arguments",
40711
+ agentsDispatched: [],
40712
+ warnings: ["Failed to parse arguments"]
40713
+ }, null, 2);
40714
+ }
40715
+ return executePhaseComplete(phaseCompleteArgs);
40716
+ }
40717
+ });
40388
40718
  // src/tools/pkg-audit.ts
40389
40719
  init_dist();
40390
- import * as fs18 from "fs";
40720
+ import * as fs19 from "fs";
40391
40721
  import * as path24 from "path";
40392
40722
  var MAX_OUTPUT_BYTES5 = 52428800;
40393
40723
  var AUDIT_TIMEOUT_MS = 120000;
@@ -40406,13 +40736,13 @@ function validateArgs3(args2) {
40406
40736
  function detectEcosystems() {
40407
40737
  const ecosystems = [];
40408
40738
  const cwd = process.cwd();
40409
- if (fs18.existsSync(path24.join(cwd, "package.json"))) {
40739
+ if (fs19.existsSync(path24.join(cwd, "package.json"))) {
40410
40740
  ecosystems.push("npm");
40411
40741
  }
40412
- if (fs18.existsSync(path24.join(cwd, "pyproject.toml")) || fs18.existsSync(path24.join(cwd, "requirements.txt"))) {
40742
+ if (fs19.existsSync(path24.join(cwd, "pyproject.toml")) || fs19.existsSync(path24.join(cwd, "requirements.txt"))) {
40413
40743
  ecosystems.push("pip");
40414
40744
  }
40415
- if (fs18.existsSync(path24.join(cwd, "Cargo.toml"))) {
40745
+ if (fs19.existsSync(path24.join(cwd, "Cargo.toml"))) {
40416
40746
  ecosystems.push("cargo");
40417
40747
  }
40418
40748
  return ecosystems;
@@ -44304,7 +44634,7 @@ init_lint();
44304
44634
  init_manager();
44305
44635
 
44306
44636
  // src/quality/metrics.ts
44307
- import * as fs19 from "fs";
44637
+ import * as fs20 from "fs";
44308
44638
  import * as path25 from "path";
44309
44639
  var MAX_FILE_SIZE_BYTES5 = 256 * 1024;
44310
44640
  var MIN_DUPLICATION_LINES = 10;
@@ -44343,11 +44673,11 @@ function estimateCyclomaticComplexity(content) {
44343
44673
  }
44344
44674
  function getComplexityForFile2(filePath) {
44345
44675
  try {
44346
- const stat = fs19.statSync(filePath);
44676
+ const stat = fs20.statSync(filePath);
44347
44677
  if (stat.size > MAX_FILE_SIZE_BYTES5) {
44348
44678
  return null;
44349
44679
  }
44350
- const content = fs19.readFileSync(filePath, "utf-8");
44680
+ const content = fs20.readFileSync(filePath, "utf-8");
44351
44681
  return estimateCyclomaticComplexity(content);
44352
44682
  } catch {
44353
44683
  return null;
@@ -44358,7 +44688,7 @@ async function computeComplexityDelta(files, workingDir) {
44358
44688
  const analyzedFiles = [];
44359
44689
  for (const file3 of files) {
44360
44690
  const fullPath = path25.isAbsolute(file3) ? file3 : path25.join(workingDir, file3);
44361
- if (!fs19.existsSync(fullPath)) {
44691
+ if (!fs20.existsSync(fullPath)) {
44362
44692
  continue;
44363
44693
  }
44364
44694
  const complexity = getComplexityForFile2(fullPath);
@@ -44479,7 +44809,7 @@ function countGoExports(content) {
44479
44809
  }
44480
44810
  function getExportCountForFile(filePath) {
44481
44811
  try {
44482
- const content = fs19.readFileSync(filePath, "utf-8");
44812
+ const content = fs20.readFileSync(filePath, "utf-8");
44483
44813
  const ext = path25.extname(filePath).toLowerCase();
44484
44814
  switch (ext) {
44485
44815
  case ".ts":
@@ -44507,7 +44837,7 @@ async function computePublicApiDelta(files, workingDir) {
44507
44837
  const analyzedFiles = [];
44508
44838
  for (const file3 of files) {
44509
44839
  const fullPath = path25.isAbsolute(file3) ? file3 : path25.join(workingDir, file3);
44510
- if (!fs19.existsSync(fullPath)) {
44840
+ if (!fs20.existsSync(fullPath)) {
44511
44841
  continue;
44512
44842
  }
44513
44843
  const exports = getExportCountForFile(fullPath);
@@ -44541,15 +44871,15 @@ async function computeDuplicationRatio(files, workingDir) {
44541
44871
  const analyzedFiles = [];
44542
44872
  for (const file3 of files) {
44543
44873
  const fullPath = path25.isAbsolute(file3) ? file3 : path25.join(workingDir, file3);
44544
- if (!fs19.existsSync(fullPath)) {
44874
+ if (!fs20.existsSync(fullPath)) {
44545
44875
  continue;
44546
44876
  }
44547
44877
  try {
44548
- const stat = fs19.statSync(fullPath);
44878
+ const stat = fs20.statSync(fullPath);
44549
44879
  if (stat.size > MAX_FILE_SIZE_BYTES5) {
44550
44880
  continue;
44551
44881
  }
44552
- const content = fs19.readFileSync(fullPath, "utf-8");
44882
+ const content = fs20.readFileSync(fullPath, "utf-8");
44553
44883
  const lines = content.split(`
44554
44884
  `).filter((line) => line.trim().length > 0);
44555
44885
  if (lines.length < MIN_DUPLICATION_LINES) {
@@ -44617,7 +44947,7 @@ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
44617
44947
  let testLines = 0;
44618
44948
  let codeLines = 0;
44619
44949
  const srcDir = path25.join(workingDir, "src");
44620
- if (fs19.existsSync(srcDir)) {
44950
+ if (fs20.existsSync(srcDir)) {
44621
44951
  await scanDirectoryForLines(srcDir, enforceGlobs, excludeGlobs, false, (lines) => {
44622
44952
  codeLines += lines;
44623
44953
  });
@@ -44625,14 +44955,14 @@ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
44625
44955
  const possibleSrcDirs = ["lib", "app", "source", "core"];
44626
44956
  for (const dir of possibleSrcDirs) {
44627
44957
  const dirPath = path25.join(workingDir, dir);
44628
- if (fs19.existsSync(dirPath)) {
44958
+ if (fs20.existsSync(dirPath)) {
44629
44959
  await scanDirectoryForLines(dirPath, enforceGlobs, excludeGlobs, false, (lines) => {
44630
44960
  codeLines += lines;
44631
44961
  });
44632
44962
  }
44633
44963
  }
44634
44964
  const testsDir = path25.join(workingDir, "tests");
44635
- if (fs19.existsSync(testsDir)) {
44965
+ if (fs20.existsSync(testsDir)) {
44636
44966
  await scanDirectoryForLines(testsDir, ["**"], ["node_modules", "dist"], true, (lines) => {
44637
44967
  testLines += lines;
44638
44968
  });
@@ -44640,7 +44970,7 @@ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
44640
44970
  const possibleTestDirs = ["test", "__tests__", "specs"];
44641
44971
  for (const dir of possibleTestDirs) {
44642
44972
  const dirPath = path25.join(workingDir, dir);
44643
- if (fs19.existsSync(dirPath) && dirPath !== testsDir) {
44973
+ if (fs20.existsSync(dirPath) && dirPath !== testsDir) {
44644
44974
  await scanDirectoryForLines(dirPath, ["**"], ["node_modules", "dist"], true, (lines) => {
44645
44975
  testLines += lines;
44646
44976
  });
@@ -44652,7 +44982,7 @@ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
44652
44982
  }
44653
44983
  async function scanDirectoryForLines(dirPath, includeGlobs, excludeGlobs, isTestScan, callback) {
44654
44984
  try {
44655
- const entries = fs19.readdirSync(dirPath, { withFileTypes: true });
44985
+ const entries = fs20.readdirSync(dirPath, { withFileTypes: true });
44656
44986
  for (const entry of entries) {
44657
44987
  const fullPath = path25.join(dirPath, entry.name);
44658
44988
  if (entry.isDirectory()) {
@@ -44700,7 +45030,7 @@ async function scanDirectoryForLines(dirPath, includeGlobs, excludeGlobs, isTest
44700
45030
  continue;
44701
45031
  }
44702
45032
  try {
44703
- const content = fs19.readFileSync(fullPath, "utf-8");
45033
+ const content = fs20.readFileSync(fullPath, "utf-8");
44704
45034
  const lines = countCodeLines(content);
44705
45035
  callback(lines);
44706
45036
  } catch {}
@@ -44916,7 +45246,7 @@ async function qualityBudget(input, directory) {
44916
45246
 
44917
45247
  // src/tools/sast-scan.ts
44918
45248
  init_manager();
44919
- import * as fs20 from "fs";
45249
+ import * as fs21 from "fs";
44920
45250
  import * as path26 from "path";
44921
45251
  import { extname as extname7 } from "path";
44922
45252
 
@@ -45780,17 +46110,17 @@ var SEVERITY_ORDER = {
45780
46110
  };
45781
46111
  function shouldSkipFile(filePath) {
45782
46112
  try {
45783
- const stats = fs20.statSync(filePath);
46113
+ const stats = fs21.statSync(filePath);
45784
46114
  if (stats.size > MAX_FILE_SIZE_BYTES6) {
45785
46115
  return { skip: true, reason: "file too large" };
45786
46116
  }
45787
46117
  if (stats.size === 0) {
45788
46118
  return { skip: true, reason: "empty file" };
45789
46119
  }
45790
- const fd = fs20.openSync(filePath, "r");
46120
+ const fd = fs21.openSync(filePath, "r");
45791
46121
  const buffer = Buffer.alloc(8192);
45792
- const bytesRead = fs20.readSync(fd, buffer, 0, 8192, 0);
45793
- fs20.closeSync(fd);
46122
+ const bytesRead = fs21.readSync(fd, buffer, 0, 8192, 0);
46123
+ fs21.closeSync(fd);
45794
46124
  if (bytesRead > 0) {
45795
46125
  let nullCount = 0;
45796
46126
  for (let i2 = 0;i2 < bytesRead; i2++) {
@@ -45829,7 +46159,7 @@ function countBySeverity(findings) {
45829
46159
  }
45830
46160
  function scanFileWithTierA(filePath, language) {
45831
46161
  try {
45832
- const content = fs20.readFileSync(filePath, "utf-8");
46162
+ const content = fs21.readFileSync(filePath, "utf-8");
45833
46163
  const findings = executeRulesSync(filePath, content, language);
45834
46164
  return findings.map((f) => ({
45835
46165
  rule_id: f.rule_id,
@@ -45873,7 +46203,7 @@ async function sastScan(input, directory, config3) {
45873
46203
  const filesByLanguage = new Map;
45874
46204
  for (const filePath of changed_files) {
45875
46205
  const resolvedPath = path26.isAbsolute(filePath) ? filePath : path26.resolve(directory, filePath);
45876
- if (!fs20.existsSync(resolvedPath)) {
46206
+ if (!fs21.existsSync(resolvedPath)) {
45877
46207
  _filesSkipped++;
45878
46208
  continue;
45879
46209
  }
@@ -46296,7 +46626,7 @@ var retrieve_summary = tool({
46296
46626
  // src/tools/sbom-generate.ts
46297
46627
  init_dist();
46298
46628
  init_manager();
46299
- import * as fs21 from "fs";
46629
+ import * as fs22 from "fs";
46300
46630
  import * as path28 from "path";
46301
46631
 
46302
46632
  // src/sbom/detectors/dart.ts
@@ -47142,7 +47472,7 @@ function findManifestFiles(rootDir) {
47142
47472
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
47143
47473
  function searchDir(dir) {
47144
47474
  try {
47145
- const entries = fs21.readdirSync(dir, { withFileTypes: true });
47475
+ const entries = fs22.readdirSync(dir, { withFileTypes: true });
47146
47476
  for (const entry of entries) {
47147
47477
  const fullPath = path28.join(dir, entry.name);
47148
47478
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "target") {
@@ -47171,7 +47501,7 @@ function findManifestFilesInDirs(directories, workingDir) {
47171
47501
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
47172
47502
  for (const dir of directories) {
47173
47503
  try {
47174
- const entries = fs21.readdirSync(dir, { withFileTypes: true });
47504
+ const entries = fs22.readdirSync(dir, { withFileTypes: true });
47175
47505
  for (const entry of entries) {
47176
47506
  const fullPath = path28.join(dir, entry.name);
47177
47507
  if (entry.isFile()) {
@@ -47209,7 +47539,7 @@ function getDirectoriesFromChangedFiles(changedFiles, workingDir) {
47209
47539
  }
47210
47540
  function ensureOutputDir(outputDir) {
47211
47541
  try {
47212
- fs21.mkdirSync(outputDir, { recursive: true });
47542
+ fs22.mkdirSync(outputDir, { recursive: true });
47213
47543
  } catch (error93) {
47214
47544
  if (!error93 || error93.code !== "EEXIST") {
47215
47545
  throw error93;
@@ -47303,10 +47633,10 @@ var sbom_generate = tool({
47303
47633
  for (const manifestFile of manifestFiles) {
47304
47634
  try {
47305
47635
  const fullPath = path28.isAbsolute(manifestFile) ? manifestFile : path28.join(workingDir, manifestFile);
47306
- if (!fs21.existsSync(fullPath)) {
47636
+ if (!fs22.existsSync(fullPath)) {
47307
47637
  continue;
47308
47638
  }
47309
- const content = fs21.readFileSync(fullPath, "utf-8");
47639
+ const content = fs22.readFileSync(fullPath, "utf-8");
47310
47640
  const components = detectComponents(manifestFile, content);
47311
47641
  processedFiles.push(manifestFile);
47312
47642
  if (components.length > 0) {
@@ -47320,7 +47650,7 @@ var sbom_generate = tool({
47320
47650
  const bomJson = serializeCycloneDX(bom);
47321
47651
  const filename = generateSbomFilename();
47322
47652
  const outputPath = path28.join(outputDir, filename);
47323
- fs21.writeFileSync(outputPath, bomJson, "utf-8");
47653
+ fs22.writeFileSync(outputPath, bomJson, "utf-8");
47324
47654
  const verdict = processedFiles.length > 0 ? "pass" : "pass";
47325
47655
  try {
47326
47656
  const timestamp = new Date().toISOString();
@@ -47361,7 +47691,7 @@ var sbom_generate = tool({
47361
47691
  });
47362
47692
  // src/tools/schema-drift.ts
47363
47693
  init_dist();
47364
- import * as fs22 from "fs";
47694
+ import * as fs23 from "fs";
47365
47695
  import * as path29 from "path";
47366
47696
  var SPEC_CANDIDATES = [
47367
47697
  "openapi.json",
@@ -47403,19 +47733,19 @@ function discoverSpecFile(cwd, specFileArg) {
47403
47733
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
47404
47734
  throw new Error(`Invalid spec_file: must end in .json, .yaml, or .yml, got ${ext}`);
47405
47735
  }
47406
- const stats = fs22.statSync(resolvedPath);
47736
+ const stats = fs23.statSync(resolvedPath);
47407
47737
  if (stats.size > MAX_SPEC_SIZE) {
47408
47738
  throw new Error(`Invalid spec_file: file exceeds ${MAX_SPEC_SIZE / 1024 / 1024}MB limit`);
47409
47739
  }
47410
- if (!fs22.existsSync(resolvedPath)) {
47740
+ if (!fs23.existsSync(resolvedPath)) {
47411
47741
  throw new Error(`Spec file not found: ${resolvedPath}`);
47412
47742
  }
47413
47743
  return resolvedPath;
47414
47744
  }
47415
47745
  for (const candidate of SPEC_CANDIDATES) {
47416
47746
  const candidatePath = path29.resolve(cwd, candidate);
47417
- if (fs22.existsSync(candidatePath)) {
47418
- const stats = fs22.statSync(candidatePath);
47747
+ if (fs23.existsSync(candidatePath)) {
47748
+ const stats = fs23.statSync(candidatePath);
47419
47749
  if (stats.size <= MAX_SPEC_SIZE) {
47420
47750
  return candidatePath;
47421
47751
  }
@@ -47424,7 +47754,7 @@ function discoverSpecFile(cwd, specFileArg) {
47424
47754
  return null;
47425
47755
  }
47426
47756
  function parseSpec(specFile) {
47427
- const content = fs22.readFileSync(specFile, "utf-8");
47757
+ const content = fs23.readFileSync(specFile, "utf-8");
47428
47758
  const ext = path29.extname(specFile).toLowerCase();
47429
47759
  if (ext === ".json") {
47430
47760
  return parseJsonSpec(content);
@@ -47491,7 +47821,7 @@ function extractRoutes(cwd) {
47491
47821
  function walkDir(dir) {
47492
47822
  let entries;
47493
47823
  try {
47494
- entries = fs22.readdirSync(dir, { withFileTypes: true });
47824
+ entries = fs23.readdirSync(dir, { withFileTypes: true });
47495
47825
  } catch {
47496
47826
  return;
47497
47827
  }
@@ -47524,7 +47854,7 @@ function extractRoutes(cwd) {
47524
47854
  }
47525
47855
  function extractRoutesFromFile(filePath) {
47526
47856
  const routes = [];
47527
- const content = fs22.readFileSync(filePath, "utf-8");
47857
+ const content = fs23.readFileSync(filePath, "utf-8");
47528
47858
  const lines = content.split(/\r?\n/);
47529
47859
  const expressRegex = /(?:app|router|server|express)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"`]([^'"`]+)['"`]/g;
47530
47860
  const flaskRegex = /@(?:app|blueprint|bp)\.route\s*\(\s*['"]([^'"]+)['"]/g;
@@ -47674,7 +48004,7 @@ init_secretscan();
47674
48004
 
47675
48005
  // src/tools/symbols.ts
47676
48006
  init_tool();
47677
- import * as fs23 from "fs";
48007
+ import * as fs24 from "fs";
47678
48008
  import * as path30 from "path";
47679
48009
  var MAX_FILE_SIZE_BYTES7 = 1024 * 1024;
47680
48010
  var WINDOWS_RESERVED_NAMES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|:|$)/i;
@@ -47705,8 +48035,8 @@ function containsWindowsAttacks(str) {
47705
48035
  function isPathInWorkspace(filePath, workspace) {
47706
48036
  try {
47707
48037
  const resolvedPath = path30.resolve(workspace, filePath);
47708
- const realWorkspace = fs23.realpathSync(workspace);
47709
- const realResolvedPath = fs23.realpathSync(resolvedPath);
48038
+ const realWorkspace = fs24.realpathSync(workspace);
48039
+ const realResolvedPath = fs24.realpathSync(resolvedPath);
47710
48040
  const relativePath = path30.relative(realWorkspace, realResolvedPath);
47711
48041
  if (relativePath.startsWith("..") || path30.isAbsolute(relativePath)) {
47712
48042
  return false;
@@ -47726,11 +48056,11 @@ function extractTSSymbols(filePath, cwd) {
47726
48056
  }
47727
48057
  let content;
47728
48058
  try {
47729
- const stats = fs23.statSync(fullPath);
48059
+ const stats = fs24.statSync(fullPath);
47730
48060
  if (stats.size > MAX_FILE_SIZE_BYTES7) {
47731
48061
  throw new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE_BYTES7})`);
47732
48062
  }
47733
- content = fs23.readFileSync(fullPath, "utf-8");
48063
+ content = fs24.readFileSync(fullPath, "utf-8");
47734
48064
  } catch {
47735
48065
  return [];
47736
48066
  }
@@ -47878,11 +48208,11 @@ function extractPythonSymbols(filePath, cwd) {
47878
48208
  }
47879
48209
  let content;
47880
48210
  try {
47881
- const stats = fs23.statSync(fullPath);
48211
+ const stats = fs24.statSync(fullPath);
47882
48212
  if (stats.size > MAX_FILE_SIZE_BYTES7) {
47883
48213
  throw new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE_BYTES7})`);
47884
48214
  }
47885
- content = fs23.readFileSync(fullPath, "utf-8");
48215
+ content = fs24.readFileSync(fullPath, "utf-8");
47886
48216
  } catch {
47887
48217
  return [];
47888
48218
  }
@@ -48022,7 +48352,7 @@ init_test_runner();
48022
48352
 
48023
48353
  // src/tools/todo-extract.ts
48024
48354
  init_dist();
48025
- import * as fs24 from "fs";
48355
+ import * as fs25 from "fs";
48026
48356
  import * as path31 from "path";
48027
48357
  var MAX_TEXT_LENGTH = 200;
48028
48358
  var MAX_FILE_SIZE_BYTES8 = 1024 * 1024;
@@ -48118,7 +48448,7 @@ function isSupportedExtension(filePath) {
48118
48448
  function findSourceFiles3(dir, files = []) {
48119
48449
  let entries;
48120
48450
  try {
48121
- entries = fs24.readdirSync(dir);
48451
+ entries = fs25.readdirSync(dir);
48122
48452
  } catch {
48123
48453
  return files;
48124
48454
  }
@@ -48130,7 +48460,7 @@ function findSourceFiles3(dir, files = []) {
48130
48460
  const fullPath = path31.join(dir, entry);
48131
48461
  let stat;
48132
48462
  try {
48133
- stat = fs24.statSync(fullPath);
48463
+ stat = fs25.statSync(fullPath);
48134
48464
  } catch {
48135
48465
  continue;
48136
48466
  }
@@ -48223,7 +48553,7 @@ var todo_extract = tool({
48223
48553
  return JSON.stringify(errorResult, null, 2);
48224
48554
  }
48225
48555
  const scanPath = resolvedPath;
48226
- if (!fs24.existsSync(scanPath)) {
48556
+ if (!fs25.existsSync(scanPath)) {
48227
48557
  const errorResult = {
48228
48558
  error: `path not found: ${pathsInput}`,
48229
48559
  total: 0,
@@ -48233,7 +48563,7 @@ var todo_extract = tool({
48233
48563
  return JSON.stringify(errorResult, null, 2);
48234
48564
  }
48235
48565
  const filesToScan = [];
48236
- const stat = fs24.statSync(scanPath);
48566
+ const stat = fs25.statSync(scanPath);
48237
48567
  if (stat.isFile()) {
48238
48568
  if (isSupportedExtension(scanPath)) {
48239
48569
  filesToScan.push(scanPath);
@@ -48252,11 +48582,11 @@ var todo_extract = tool({
48252
48582
  const allEntries = [];
48253
48583
  for (const filePath of filesToScan) {
48254
48584
  try {
48255
- const fileStat = fs24.statSync(filePath);
48585
+ const fileStat = fs25.statSync(filePath);
48256
48586
  if (fileStat.size > MAX_FILE_SIZE_BYTES8) {
48257
48587
  continue;
48258
48588
  }
48259
- const content = fs24.readFileSync(filePath, "utf-8");
48589
+ const content = fs25.readFileSync(filePath, "utf-8");
48260
48590
  const entries = parseTodoComments(content, filePath, tagsSet);
48261
48591
  allEntries.push(...entries);
48262
48592
  } catch {}
@@ -48457,6 +48787,7 @@ var OpenCodeSwarm = async (ctx) => {
48457
48787
  lint,
48458
48788
  diff,
48459
48789
  pkg_audit,
48790
+ phase_complete,
48460
48791
  pre_check_batch,
48461
48792
  retrieve_summary,
48462
48793
  schema_drift,
@@ -48474,7 +48805,7 @@ var OpenCodeSwarm = async (ctx) => {
48474
48805
  opencodeConfig.command = {
48475
48806
  ...opencodeConfig.command || {},
48476
48807
  swarm: {
48477
- template: "{{arguments}}",
48808
+ template: "/swarm $ARGUMENTS",
48478
48809
  description: "Swarm management commands"
48479
48810
  }
48480
48811
  };
package/dist/state.d.ts CHANGED
@@ -75,6 +75,12 @@ export interface AgentSessionState {
75
75
  selfFixAttempted: boolean;
76
76
  /** Phases that have already received a catastrophic zero-reviewer warning */
77
77
  catastrophicPhaseWarnings: Set<number>;
78
+ /** Timestamp of most recent phase completion */
79
+ lastPhaseCompleteTimestamp: number;
80
+ /** Phase number of most recent phase completion */
81
+ lastPhaseCompletePhase: number;
82
+ /** Set of agents dispatched in current phase (normalized names) */
83
+ phaseAgentsDispatched: Set<string>;
78
84
  }
79
85
  /**
80
86
  * Represents a single agent invocation window with isolated guardrail budgets.
@@ -190,3 +196,10 @@ export declare function getActiveWindow(sessionId: string): InvocationWindow | u
190
196
  * @param maxWindows - Maximum number of windows to keep (default 50)
191
197
  */
192
198
  export declare function pruneOldWindows(sessionId: string, maxAgeMs?: number, maxWindows?: number): void;
199
+ /**
200
+ * Record an agent dispatch for phase completion tracking.
201
+ * Normalizes the agent name via stripKnownSwarmPrefix before adding to phaseAgentsDispatched.
202
+ * @param sessionId - Session identifier
203
+ * @param agentName - Agent name to record (will be normalized)
204
+ */
205
+ export declare function recordPhaseAgentDispatch(sessionId: string, agentName: string): void;
@@ -8,6 +8,7 @@ export { extract_code_blocks } from './file-extractor';
8
8
  export { fetchGitingest, type GitingestArgs, gitingest } from './gitingest';
9
9
  export { imports } from './imports';
10
10
  export { lint } from './lint';
11
+ export { phase_complete } from './phase-complete';
11
12
  export { pkg_audit } from './pkg-audit';
12
13
  export { type PlaceholderFinding, type PlaceholderScanInput, type PlaceholderScanResult, placeholderScan, } from './placeholder-scan';
13
14
  export { type PreCheckBatchInput, type PreCheckBatchResult, pre_check_batch, runPreCheckBatch, type ToolResult, } from './pre-check-batch';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Phase completion tool for tracking and validating phase completion.
3
+ * Core implementation - gathers data, enforces policy, writes event, resets state.
4
+ */
5
+ import { type ToolDefinition } from '@opencode-ai/plugin/tool';
6
+ /**
7
+ * Tool definition for phase_complete
8
+ */
9
+ export declare const phase_complete: ToolDefinition;
@@ -3,7 +3,7 @@
3
3
  * Used for constants and agent setup references.
4
4
  */
5
5
  /** Union type of all valid tool names */
6
- export type ToolName = 'diff' | 'syntax_check' | 'placeholder_scan' | 'imports' | 'lint' | 'secretscan' | 'sast_scan' | 'build_check' | 'pre_check_batch' | 'quality_budget' | 'symbols' | 'complexity_hotspots' | 'schema_drift' | 'todo_extract' | 'evidence_check' | 'sbom_generate' | 'checkpoint' | 'pkg_audit' | 'test_runner' | 'detect_domains' | 'gitingest' | 'retrieve_summary' | 'extract_code_blocks';
6
+ export type ToolName = 'diff' | 'syntax_check' | 'placeholder_scan' | 'imports' | 'lint' | 'secretscan' | 'sast_scan' | 'build_check' | 'pre_check_batch' | 'quality_budget' | 'symbols' | 'complexity_hotspots' | 'schema_drift' | 'todo_extract' | 'evidence_check' | 'sbom_generate' | 'checkpoint' | 'pkg_audit' | 'test_runner' | 'detect_domains' | 'gitingest' | 'retrieve_summary' | 'extract_code_blocks' | 'phase_complete';
7
7
  /** Readonly array of all tool names */
8
8
  export declare const TOOL_NAMES: readonly ToolName[];
9
9
  /** Set for O(1) tool name validation */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "6.13.0",
3
+ "version": "6.13.2",
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",
@@ -9,6 +9,14 @@
9
9
  },
10
10
  "type": "module",
11
11
  "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/zaxbysauce/opencode-swarm.git"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org/"
19
+ },
12
20
  "keywords": [
13
21
  "opencode",
14
22
  "opencode-plugin",