opencode-swarm 6.13.1 → 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
 
@@ -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
 
@@ -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(),
@@ -34451,7 +34466,10 @@ function startAgentSession(sessionId, agentName, staleDurationMs = 7200000) {
34451
34466
  lastGateFailure: null,
34452
34467
  partialGateWarningIssued: false,
34453
34468
  selfFixAttempted: false,
34454
- catastrophicPhaseWarnings: new Set
34469
+ catastrophicPhaseWarnings: new Set,
34470
+ lastPhaseCompleteTimestamp: 0,
34471
+ lastPhaseCompletePhase: 0,
34472
+ phaseAgentsDispatched: new Set
34455
34473
  };
34456
34474
  swarmState.agentSessions.set(sessionId, sessionState);
34457
34475
  swarmState.activeAgent.set(sessionId, agentName);
@@ -34502,6 +34520,15 @@ function ensureAgentSession(sessionId, agentName) {
34502
34520
  if (!session.catastrophicPhaseWarnings) {
34503
34521
  session.catastrophicPhaseWarnings = new Set;
34504
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
+ }
34505
34532
  session.lastToolCallTime = now;
34506
34533
  return session;
34507
34534
  }
@@ -34570,6 +34597,17 @@ function pruneOldWindows(sessionId, maxAgeMs = 24 * 60 * 60 * 1000, maxWindows =
34570
34597
  const toKeep = sorted.slice(0, maxWindows);
34571
34598
  session.windows = Object.fromEntries(toKeep);
34572
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
+ }
34573
34611
 
34574
34612
  // src/commands/benchmark.ts
34575
34613
  init_utils();
@@ -36680,6 +36718,7 @@ function createDelegationTrackerHook(config3, guardrailsEnabled = true) {
36680
36718
  const isArchitect = strippedAgent === ORCHESTRATOR_NAME;
36681
36719
  const session = ensureAgentSession(input.sessionID, agentName);
36682
36720
  session.delegationActive = !isArchitect;
36721
+ recordPhaseAgentDispatch(input.sessionID, agentName);
36683
36722
  if (!isArchitect && guardrailsEnabled) {
36684
36723
  beginInvocation(input.sessionID, agentName);
36685
36724
  }
@@ -37131,8 +37170,8 @@ function hashArgs(args2) {
37131
37170
  // src/hooks/messages-transform.ts
37132
37171
  function consolidateSystemMessages(messages) {
37133
37172
  if (messages.length > 0 && messages[0].role === "system" && messages[0].content !== undefined && typeof messages[0].content === "string" && messages[0].content.trim().length > 0) {
37134
- 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;
37135
- if (systemMessageCount === 1) {
37173
+ const totalSystemCount = messages.filter((m) => m.role === "system").length;
37174
+ if (totalSystemCount === 1) {
37136
37175
  return [...messages];
37137
37176
  }
37138
37177
  }
@@ -37140,34 +37179,31 @@ function consolidateSystemMessages(messages) {
37140
37179
  const systemContents = [];
37141
37180
  for (let i2 = 0;i2 < messages.length; i2++) {
37142
37181
  const message = messages[i2];
37143
- if (message.role !== "system") {
37144
- continue;
37145
- }
37146
- if (message.tool_call_id !== undefined || message.name !== undefined) {
37147
- continue;
37148
- }
37149
- if (typeof message.content !== "string") {
37182
+ if (message.role !== "system")
37150
37183
  continue;
37151
- }
37152
- const trimmedContent = message.content.trim();
37153
- if (trimmedContent.length === 0) {
37184
+ if (message.tool_call_id !== undefined || message.name !== undefined)
37154
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
+ `);
37155
37196
  }
37156
37197
  systemMessageIndices.push(i2);
37157
- systemContents.push(trimmedContent);
37198
+ if (textContent) {
37199
+ systemContents.push(textContent);
37200
+ }
37158
37201
  }
37159
37202
  if (systemContents.length === 0) {
37160
- return messages.filter((m) => {
37161
- if (m.role !== "system") {
37162
- return true;
37163
- }
37164
- if (typeof m.content !== "string" || m.name !== undefined || m.tool_call_id !== undefined) {
37203
+ return messages.filter((m, idx) => {
37204
+ if (m.role !== "system")
37165
37205
  return true;
37166
- }
37167
- if (m.content.trim().length > 0) {
37168
- return true;
37169
- }
37170
- return false;
37206
+ return idx === 0;
37171
37207
  });
37172
37208
  }
37173
37209
  const mergedSystemContent = systemContents.join(`
@@ -37178,7 +37214,7 @@ function consolidateSystemMessages(messages) {
37178
37214
  result.push({
37179
37215
  role: "system",
37180
37216
  content: mergedSystemContent,
37181
- ...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"))
37182
37218
  });
37183
37219
  for (let i2 = 0;i2 < messages.length; i2++) {
37184
37220
  const message = messages[i2];
@@ -37190,7 +37226,11 @@ function consolidateSystemMessages(messages) {
37190
37226
  }
37191
37227
  result.push({ ...message });
37192
37228
  }
37193
- return result;
37229
+ return result.filter((msg, idx) => {
37230
+ if (idx === 0)
37231
+ return true;
37232
+ return msg.role !== "system";
37233
+ });
37194
37234
  }
37195
37235
  // src/hooks/phase-monitor.ts
37196
37236
  init_manager2();
@@ -37585,6 +37625,39 @@ init_preflight_service();
37585
37625
  // src/hooks/system-enhancer.ts
37586
37626
  init_utils();
37587
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
+
37588
37661
  // src/hooks/context-scoring.ts
37589
37662
  function calculateAgeFactor(ageHours, config3) {
37590
37663
  if (ageHours <= 0) {
@@ -37727,6 +37800,35 @@ function createSystemEnhancerHook(config3, directory) {
37727
37800
  if (config3.secretscan?.enabled === false) {
37728
37801
  tryInject("[SWARM CONFIG] Secretscan gate is DISABLED. Skip secretscan in QA sequence.");
37729
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
+ }
37730
37832
  if (mode !== "DISCOVER") {
37731
37833
  const sessionId_preflight = _input.sessionID;
37732
37834
  const activeAgent_preflight = swarmState.activeAgent.get(sessionId_preflight ?? "");
@@ -37988,6 +38090,34 @@ function createSystemEnhancerHook(config3, directory) {
37988
38090
  metadata: { contentType: "prose" }
37989
38091
  });
37990
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
+ }
37991
38121
  const sessionId_preflight_b = _input.sessionID;
37992
38122
  const activeAgent_preflight_b = swarmState.activeAgent.get(sessionId_preflight_b ?? "");
37993
38123
  const isArchitectForPreflight_b = !activeAgent_preflight_b || stripKnownSwarmPrefix(activeAgent_preflight_b) === "architect";
@@ -38306,6 +38436,10 @@ function createToolSummarizerHook(config3, directory) {
38306
38436
  if (typeof output.output !== "string" || output.output.length === 0) {
38307
38437
  return;
38308
38438
  }
38439
+ const exemptTools = config3.exempt_tools ?? ["retrieve_summary", "task"];
38440
+ if (exemptTools.includes(input.tool)) {
38441
+ return;
38442
+ }
38309
38443
  if (!shouldSummarize(output.output, config3.threshold_bytes)) {
38310
38444
  return;
38311
38445
  }
@@ -40418,9 +40552,172 @@ var imports = tool({
40418
40552
  // src/tools/index.ts
40419
40553
  init_lint();
40420
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
+ });
40421
40718
  // src/tools/pkg-audit.ts
40422
40719
  init_dist();
40423
- import * as fs18 from "fs";
40720
+ import * as fs19 from "fs";
40424
40721
  import * as path24 from "path";
40425
40722
  var MAX_OUTPUT_BYTES5 = 52428800;
40426
40723
  var AUDIT_TIMEOUT_MS = 120000;
@@ -40439,13 +40736,13 @@ function validateArgs3(args2) {
40439
40736
  function detectEcosystems() {
40440
40737
  const ecosystems = [];
40441
40738
  const cwd = process.cwd();
40442
- if (fs18.existsSync(path24.join(cwd, "package.json"))) {
40739
+ if (fs19.existsSync(path24.join(cwd, "package.json"))) {
40443
40740
  ecosystems.push("npm");
40444
40741
  }
40445
- 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"))) {
40446
40743
  ecosystems.push("pip");
40447
40744
  }
40448
- if (fs18.existsSync(path24.join(cwd, "Cargo.toml"))) {
40745
+ if (fs19.existsSync(path24.join(cwd, "Cargo.toml"))) {
40449
40746
  ecosystems.push("cargo");
40450
40747
  }
40451
40748
  return ecosystems;
@@ -44337,7 +44634,7 @@ init_lint();
44337
44634
  init_manager();
44338
44635
 
44339
44636
  // src/quality/metrics.ts
44340
- import * as fs19 from "fs";
44637
+ import * as fs20 from "fs";
44341
44638
  import * as path25 from "path";
44342
44639
  var MAX_FILE_SIZE_BYTES5 = 256 * 1024;
44343
44640
  var MIN_DUPLICATION_LINES = 10;
@@ -44376,11 +44673,11 @@ function estimateCyclomaticComplexity(content) {
44376
44673
  }
44377
44674
  function getComplexityForFile2(filePath) {
44378
44675
  try {
44379
- const stat = fs19.statSync(filePath);
44676
+ const stat = fs20.statSync(filePath);
44380
44677
  if (stat.size > MAX_FILE_SIZE_BYTES5) {
44381
44678
  return null;
44382
44679
  }
44383
- const content = fs19.readFileSync(filePath, "utf-8");
44680
+ const content = fs20.readFileSync(filePath, "utf-8");
44384
44681
  return estimateCyclomaticComplexity(content);
44385
44682
  } catch {
44386
44683
  return null;
@@ -44391,7 +44688,7 @@ async function computeComplexityDelta(files, workingDir) {
44391
44688
  const analyzedFiles = [];
44392
44689
  for (const file3 of files) {
44393
44690
  const fullPath = path25.isAbsolute(file3) ? file3 : path25.join(workingDir, file3);
44394
- if (!fs19.existsSync(fullPath)) {
44691
+ if (!fs20.existsSync(fullPath)) {
44395
44692
  continue;
44396
44693
  }
44397
44694
  const complexity = getComplexityForFile2(fullPath);
@@ -44512,7 +44809,7 @@ function countGoExports(content) {
44512
44809
  }
44513
44810
  function getExportCountForFile(filePath) {
44514
44811
  try {
44515
- const content = fs19.readFileSync(filePath, "utf-8");
44812
+ const content = fs20.readFileSync(filePath, "utf-8");
44516
44813
  const ext = path25.extname(filePath).toLowerCase();
44517
44814
  switch (ext) {
44518
44815
  case ".ts":
@@ -44540,7 +44837,7 @@ async function computePublicApiDelta(files, workingDir) {
44540
44837
  const analyzedFiles = [];
44541
44838
  for (const file3 of files) {
44542
44839
  const fullPath = path25.isAbsolute(file3) ? file3 : path25.join(workingDir, file3);
44543
- if (!fs19.existsSync(fullPath)) {
44840
+ if (!fs20.existsSync(fullPath)) {
44544
44841
  continue;
44545
44842
  }
44546
44843
  const exports = getExportCountForFile(fullPath);
@@ -44574,15 +44871,15 @@ async function computeDuplicationRatio(files, workingDir) {
44574
44871
  const analyzedFiles = [];
44575
44872
  for (const file3 of files) {
44576
44873
  const fullPath = path25.isAbsolute(file3) ? file3 : path25.join(workingDir, file3);
44577
- if (!fs19.existsSync(fullPath)) {
44874
+ if (!fs20.existsSync(fullPath)) {
44578
44875
  continue;
44579
44876
  }
44580
44877
  try {
44581
- const stat = fs19.statSync(fullPath);
44878
+ const stat = fs20.statSync(fullPath);
44582
44879
  if (stat.size > MAX_FILE_SIZE_BYTES5) {
44583
44880
  continue;
44584
44881
  }
44585
- const content = fs19.readFileSync(fullPath, "utf-8");
44882
+ const content = fs20.readFileSync(fullPath, "utf-8");
44586
44883
  const lines = content.split(`
44587
44884
  `).filter((line) => line.trim().length > 0);
44588
44885
  if (lines.length < MIN_DUPLICATION_LINES) {
@@ -44650,7 +44947,7 @@ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
44650
44947
  let testLines = 0;
44651
44948
  let codeLines = 0;
44652
44949
  const srcDir = path25.join(workingDir, "src");
44653
- if (fs19.existsSync(srcDir)) {
44950
+ if (fs20.existsSync(srcDir)) {
44654
44951
  await scanDirectoryForLines(srcDir, enforceGlobs, excludeGlobs, false, (lines) => {
44655
44952
  codeLines += lines;
44656
44953
  });
@@ -44658,14 +44955,14 @@ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
44658
44955
  const possibleSrcDirs = ["lib", "app", "source", "core"];
44659
44956
  for (const dir of possibleSrcDirs) {
44660
44957
  const dirPath = path25.join(workingDir, dir);
44661
- if (fs19.existsSync(dirPath)) {
44958
+ if (fs20.existsSync(dirPath)) {
44662
44959
  await scanDirectoryForLines(dirPath, enforceGlobs, excludeGlobs, false, (lines) => {
44663
44960
  codeLines += lines;
44664
44961
  });
44665
44962
  }
44666
44963
  }
44667
44964
  const testsDir = path25.join(workingDir, "tests");
44668
- if (fs19.existsSync(testsDir)) {
44965
+ if (fs20.existsSync(testsDir)) {
44669
44966
  await scanDirectoryForLines(testsDir, ["**"], ["node_modules", "dist"], true, (lines) => {
44670
44967
  testLines += lines;
44671
44968
  });
@@ -44673,7 +44970,7 @@ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
44673
44970
  const possibleTestDirs = ["test", "__tests__", "specs"];
44674
44971
  for (const dir of possibleTestDirs) {
44675
44972
  const dirPath = path25.join(workingDir, dir);
44676
- if (fs19.existsSync(dirPath) && dirPath !== testsDir) {
44973
+ if (fs20.existsSync(dirPath) && dirPath !== testsDir) {
44677
44974
  await scanDirectoryForLines(dirPath, ["**"], ["node_modules", "dist"], true, (lines) => {
44678
44975
  testLines += lines;
44679
44976
  });
@@ -44685,7 +44982,7 @@ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
44685
44982
  }
44686
44983
  async function scanDirectoryForLines(dirPath, includeGlobs, excludeGlobs, isTestScan, callback) {
44687
44984
  try {
44688
- const entries = fs19.readdirSync(dirPath, { withFileTypes: true });
44985
+ const entries = fs20.readdirSync(dirPath, { withFileTypes: true });
44689
44986
  for (const entry of entries) {
44690
44987
  const fullPath = path25.join(dirPath, entry.name);
44691
44988
  if (entry.isDirectory()) {
@@ -44733,7 +45030,7 @@ async function scanDirectoryForLines(dirPath, includeGlobs, excludeGlobs, isTest
44733
45030
  continue;
44734
45031
  }
44735
45032
  try {
44736
- const content = fs19.readFileSync(fullPath, "utf-8");
45033
+ const content = fs20.readFileSync(fullPath, "utf-8");
44737
45034
  const lines = countCodeLines(content);
44738
45035
  callback(lines);
44739
45036
  } catch {}
@@ -44949,7 +45246,7 @@ async function qualityBudget(input, directory) {
44949
45246
 
44950
45247
  // src/tools/sast-scan.ts
44951
45248
  init_manager();
44952
- import * as fs20 from "fs";
45249
+ import * as fs21 from "fs";
44953
45250
  import * as path26 from "path";
44954
45251
  import { extname as extname7 } from "path";
44955
45252
 
@@ -45813,17 +46110,17 @@ var SEVERITY_ORDER = {
45813
46110
  };
45814
46111
  function shouldSkipFile(filePath) {
45815
46112
  try {
45816
- const stats = fs20.statSync(filePath);
46113
+ const stats = fs21.statSync(filePath);
45817
46114
  if (stats.size > MAX_FILE_SIZE_BYTES6) {
45818
46115
  return { skip: true, reason: "file too large" };
45819
46116
  }
45820
46117
  if (stats.size === 0) {
45821
46118
  return { skip: true, reason: "empty file" };
45822
46119
  }
45823
- const fd = fs20.openSync(filePath, "r");
46120
+ const fd = fs21.openSync(filePath, "r");
45824
46121
  const buffer = Buffer.alloc(8192);
45825
- const bytesRead = fs20.readSync(fd, buffer, 0, 8192, 0);
45826
- fs20.closeSync(fd);
46122
+ const bytesRead = fs21.readSync(fd, buffer, 0, 8192, 0);
46123
+ fs21.closeSync(fd);
45827
46124
  if (bytesRead > 0) {
45828
46125
  let nullCount = 0;
45829
46126
  for (let i2 = 0;i2 < bytesRead; i2++) {
@@ -45862,7 +46159,7 @@ function countBySeverity(findings) {
45862
46159
  }
45863
46160
  function scanFileWithTierA(filePath, language) {
45864
46161
  try {
45865
- const content = fs20.readFileSync(filePath, "utf-8");
46162
+ const content = fs21.readFileSync(filePath, "utf-8");
45866
46163
  const findings = executeRulesSync(filePath, content, language);
45867
46164
  return findings.map((f) => ({
45868
46165
  rule_id: f.rule_id,
@@ -45906,7 +46203,7 @@ async function sastScan(input, directory, config3) {
45906
46203
  const filesByLanguage = new Map;
45907
46204
  for (const filePath of changed_files) {
45908
46205
  const resolvedPath = path26.isAbsolute(filePath) ? filePath : path26.resolve(directory, filePath);
45909
- if (!fs20.existsSync(resolvedPath)) {
46206
+ if (!fs21.existsSync(resolvedPath)) {
45910
46207
  _filesSkipped++;
45911
46208
  continue;
45912
46209
  }
@@ -46329,7 +46626,7 @@ var retrieve_summary = tool({
46329
46626
  // src/tools/sbom-generate.ts
46330
46627
  init_dist();
46331
46628
  init_manager();
46332
- import * as fs21 from "fs";
46629
+ import * as fs22 from "fs";
46333
46630
  import * as path28 from "path";
46334
46631
 
46335
46632
  // src/sbom/detectors/dart.ts
@@ -47175,7 +47472,7 @@ function findManifestFiles(rootDir) {
47175
47472
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
47176
47473
  function searchDir(dir) {
47177
47474
  try {
47178
- const entries = fs21.readdirSync(dir, { withFileTypes: true });
47475
+ const entries = fs22.readdirSync(dir, { withFileTypes: true });
47179
47476
  for (const entry of entries) {
47180
47477
  const fullPath = path28.join(dir, entry.name);
47181
47478
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "target") {
@@ -47204,7 +47501,7 @@ function findManifestFilesInDirs(directories, workingDir) {
47204
47501
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
47205
47502
  for (const dir of directories) {
47206
47503
  try {
47207
- const entries = fs21.readdirSync(dir, { withFileTypes: true });
47504
+ const entries = fs22.readdirSync(dir, { withFileTypes: true });
47208
47505
  for (const entry of entries) {
47209
47506
  const fullPath = path28.join(dir, entry.name);
47210
47507
  if (entry.isFile()) {
@@ -47242,7 +47539,7 @@ function getDirectoriesFromChangedFiles(changedFiles, workingDir) {
47242
47539
  }
47243
47540
  function ensureOutputDir(outputDir) {
47244
47541
  try {
47245
- fs21.mkdirSync(outputDir, { recursive: true });
47542
+ fs22.mkdirSync(outputDir, { recursive: true });
47246
47543
  } catch (error93) {
47247
47544
  if (!error93 || error93.code !== "EEXIST") {
47248
47545
  throw error93;
@@ -47336,10 +47633,10 @@ var sbom_generate = tool({
47336
47633
  for (const manifestFile of manifestFiles) {
47337
47634
  try {
47338
47635
  const fullPath = path28.isAbsolute(manifestFile) ? manifestFile : path28.join(workingDir, manifestFile);
47339
- if (!fs21.existsSync(fullPath)) {
47636
+ if (!fs22.existsSync(fullPath)) {
47340
47637
  continue;
47341
47638
  }
47342
- const content = fs21.readFileSync(fullPath, "utf-8");
47639
+ const content = fs22.readFileSync(fullPath, "utf-8");
47343
47640
  const components = detectComponents(manifestFile, content);
47344
47641
  processedFiles.push(manifestFile);
47345
47642
  if (components.length > 0) {
@@ -47353,7 +47650,7 @@ var sbom_generate = tool({
47353
47650
  const bomJson = serializeCycloneDX(bom);
47354
47651
  const filename = generateSbomFilename();
47355
47652
  const outputPath = path28.join(outputDir, filename);
47356
- fs21.writeFileSync(outputPath, bomJson, "utf-8");
47653
+ fs22.writeFileSync(outputPath, bomJson, "utf-8");
47357
47654
  const verdict = processedFiles.length > 0 ? "pass" : "pass";
47358
47655
  try {
47359
47656
  const timestamp = new Date().toISOString();
@@ -47394,7 +47691,7 @@ var sbom_generate = tool({
47394
47691
  });
47395
47692
  // src/tools/schema-drift.ts
47396
47693
  init_dist();
47397
- import * as fs22 from "fs";
47694
+ import * as fs23 from "fs";
47398
47695
  import * as path29 from "path";
47399
47696
  var SPEC_CANDIDATES = [
47400
47697
  "openapi.json",
@@ -47436,19 +47733,19 @@ function discoverSpecFile(cwd, specFileArg) {
47436
47733
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
47437
47734
  throw new Error(`Invalid spec_file: must end in .json, .yaml, or .yml, got ${ext}`);
47438
47735
  }
47439
- const stats = fs22.statSync(resolvedPath);
47736
+ const stats = fs23.statSync(resolvedPath);
47440
47737
  if (stats.size > MAX_SPEC_SIZE) {
47441
47738
  throw new Error(`Invalid spec_file: file exceeds ${MAX_SPEC_SIZE / 1024 / 1024}MB limit`);
47442
47739
  }
47443
- if (!fs22.existsSync(resolvedPath)) {
47740
+ if (!fs23.existsSync(resolvedPath)) {
47444
47741
  throw new Error(`Spec file not found: ${resolvedPath}`);
47445
47742
  }
47446
47743
  return resolvedPath;
47447
47744
  }
47448
47745
  for (const candidate of SPEC_CANDIDATES) {
47449
47746
  const candidatePath = path29.resolve(cwd, candidate);
47450
- if (fs22.existsSync(candidatePath)) {
47451
- const stats = fs22.statSync(candidatePath);
47747
+ if (fs23.existsSync(candidatePath)) {
47748
+ const stats = fs23.statSync(candidatePath);
47452
47749
  if (stats.size <= MAX_SPEC_SIZE) {
47453
47750
  return candidatePath;
47454
47751
  }
@@ -47457,7 +47754,7 @@ function discoverSpecFile(cwd, specFileArg) {
47457
47754
  return null;
47458
47755
  }
47459
47756
  function parseSpec(specFile) {
47460
- const content = fs22.readFileSync(specFile, "utf-8");
47757
+ const content = fs23.readFileSync(specFile, "utf-8");
47461
47758
  const ext = path29.extname(specFile).toLowerCase();
47462
47759
  if (ext === ".json") {
47463
47760
  return parseJsonSpec(content);
@@ -47524,7 +47821,7 @@ function extractRoutes(cwd) {
47524
47821
  function walkDir(dir) {
47525
47822
  let entries;
47526
47823
  try {
47527
- entries = fs22.readdirSync(dir, { withFileTypes: true });
47824
+ entries = fs23.readdirSync(dir, { withFileTypes: true });
47528
47825
  } catch {
47529
47826
  return;
47530
47827
  }
@@ -47557,7 +47854,7 @@ function extractRoutes(cwd) {
47557
47854
  }
47558
47855
  function extractRoutesFromFile(filePath) {
47559
47856
  const routes = [];
47560
- const content = fs22.readFileSync(filePath, "utf-8");
47857
+ const content = fs23.readFileSync(filePath, "utf-8");
47561
47858
  const lines = content.split(/\r?\n/);
47562
47859
  const expressRegex = /(?:app|router|server|express)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"`]([^'"`]+)['"`]/g;
47563
47860
  const flaskRegex = /@(?:app|blueprint|bp)\.route\s*\(\s*['"]([^'"]+)['"]/g;
@@ -47707,7 +48004,7 @@ init_secretscan();
47707
48004
 
47708
48005
  // src/tools/symbols.ts
47709
48006
  init_tool();
47710
- import * as fs23 from "fs";
48007
+ import * as fs24 from "fs";
47711
48008
  import * as path30 from "path";
47712
48009
  var MAX_FILE_SIZE_BYTES7 = 1024 * 1024;
47713
48010
  var WINDOWS_RESERVED_NAMES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|:|$)/i;
@@ -47738,8 +48035,8 @@ function containsWindowsAttacks(str) {
47738
48035
  function isPathInWorkspace(filePath, workspace) {
47739
48036
  try {
47740
48037
  const resolvedPath = path30.resolve(workspace, filePath);
47741
- const realWorkspace = fs23.realpathSync(workspace);
47742
- const realResolvedPath = fs23.realpathSync(resolvedPath);
48038
+ const realWorkspace = fs24.realpathSync(workspace);
48039
+ const realResolvedPath = fs24.realpathSync(resolvedPath);
47743
48040
  const relativePath = path30.relative(realWorkspace, realResolvedPath);
47744
48041
  if (relativePath.startsWith("..") || path30.isAbsolute(relativePath)) {
47745
48042
  return false;
@@ -47759,11 +48056,11 @@ function extractTSSymbols(filePath, cwd) {
47759
48056
  }
47760
48057
  let content;
47761
48058
  try {
47762
- const stats = fs23.statSync(fullPath);
48059
+ const stats = fs24.statSync(fullPath);
47763
48060
  if (stats.size > MAX_FILE_SIZE_BYTES7) {
47764
48061
  throw new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE_BYTES7})`);
47765
48062
  }
47766
- content = fs23.readFileSync(fullPath, "utf-8");
48063
+ content = fs24.readFileSync(fullPath, "utf-8");
47767
48064
  } catch {
47768
48065
  return [];
47769
48066
  }
@@ -47911,11 +48208,11 @@ function extractPythonSymbols(filePath, cwd) {
47911
48208
  }
47912
48209
  let content;
47913
48210
  try {
47914
- const stats = fs23.statSync(fullPath);
48211
+ const stats = fs24.statSync(fullPath);
47915
48212
  if (stats.size > MAX_FILE_SIZE_BYTES7) {
47916
48213
  throw new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE_BYTES7})`);
47917
48214
  }
47918
- content = fs23.readFileSync(fullPath, "utf-8");
48215
+ content = fs24.readFileSync(fullPath, "utf-8");
47919
48216
  } catch {
47920
48217
  return [];
47921
48218
  }
@@ -48055,7 +48352,7 @@ init_test_runner();
48055
48352
 
48056
48353
  // src/tools/todo-extract.ts
48057
48354
  init_dist();
48058
- import * as fs24 from "fs";
48355
+ import * as fs25 from "fs";
48059
48356
  import * as path31 from "path";
48060
48357
  var MAX_TEXT_LENGTH = 200;
48061
48358
  var MAX_FILE_SIZE_BYTES8 = 1024 * 1024;
@@ -48151,7 +48448,7 @@ function isSupportedExtension(filePath) {
48151
48448
  function findSourceFiles3(dir, files = []) {
48152
48449
  let entries;
48153
48450
  try {
48154
- entries = fs24.readdirSync(dir);
48451
+ entries = fs25.readdirSync(dir);
48155
48452
  } catch {
48156
48453
  return files;
48157
48454
  }
@@ -48163,7 +48460,7 @@ function findSourceFiles3(dir, files = []) {
48163
48460
  const fullPath = path31.join(dir, entry);
48164
48461
  let stat;
48165
48462
  try {
48166
- stat = fs24.statSync(fullPath);
48463
+ stat = fs25.statSync(fullPath);
48167
48464
  } catch {
48168
48465
  continue;
48169
48466
  }
@@ -48256,7 +48553,7 @@ var todo_extract = tool({
48256
48553
  return JSON.stringify(errorResult, null, 2);
48257
48554
  }
48258
48555
  const scanPath = resolvedPath;
48259
- if (!fs24.existsSync(scanPath)) {
48556
+ if (!fs25.existsSync(scanPath)) {
48260
48557
  const errorResult = {
48261
48558
  error: `path not found: ${pathsInput}`,
48262
48559
  total: 0,
@@ -48266,7 +48563,7 @@ var todo_extract = tool({
48266
48563
  return JSON.stringify(errorResult, null, 2);
48267
48564
  }
48268
48565
  const filesToScan = [];
48269
- const stat = fs24.statSync(scanPath);
48566
+ const stat = fs25.statSync(scanPath);
48270
48567
  if (stat.isFile()) {
48271
48568
  if (isSupportedExtension(scanPath)) {
48272
48569
  filesToScan.push(scanPath);
@@ -48285,11 +48582,11 @@ var todo_extract = tool({
48285
48582
  const allEntries = [];
48286
48583
  for (const filePath of filesToScan) {
48287
48584
  try {
48288
- const fileStat = fs24.statSync(filePath);
48585
+ const fileStat = fs25.statSync(filePath);
48289
48586
  if (fileStat.size > MAX_FILE_SIZE_BYTES8) {
48290
48587
  continue;
48291
48588
  }
48292
- const content = fs24.readFileSync(filePath, "utf-8");
48589
+ const content = fs25.readFileSync(filePath, "utf-8");
48293
48590
  const entries = parseTodoComments(content, filePath, tagsSet);
48294
48591
  allEntries.push(...entries);
48295
48592
  } catch {}
@@ -48490,6 +48787,7 @@ var OpenCodeSwarm = async (ctx) => {
48490
48787
  lint,
48491
48788
  diff,
48492
48789
  pkg_audit,
48790
+ phase_complete,
48493
48791
  pre_check_batch,
48494
48792
  retrieve_summary,
48495
48793
  schema_drift,
@@ -48507,7 +48805,7 @@ var OpenCodeSwarm = async (ctx) => {
48507
48805
  opencodeConfig.command = {
48508
48806
  ...opencodeConfig.command || {},
48509
48807
  swarm: {
48510
- template: 'The /swarm command has been processed by the plugin handler. Acknowledge with: "Done." Do not take any further action. User input was: $ARGUMENTS',
48808
+ template: "/swarm $ARGUMENTS",
48511
48809
  description: "Swarm management commands"
48512
48810
  }
48513
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.1",
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",