opencode-swarm 5.1.1 → 5.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <p align="center">
2
- <img src="https://img.shields.io/badge/version-5.0.0-blue" alt="Version">
2
+ <img src="https://img.shields.io/badge/version-5.1.4-blue" alt="Version">
3
3
  <img src="https://img.shields.io/badge/license-MIT-green" alt="License">
4
4
  <img src="https://img.shields.io/badge/opencode-plugin-purple" alt="OpenCode Plugin">
5
- <img src="https://img.shields.io/badge/agents-8-orange" alt="Agents">
6
- <img src="https://img.shields.io/badge/tests-876-brightgreen" alt="Tests">
5
+ <img src="https://img.shields.io/badge/agents-7-orange" alt="Agents">
6
+ <img src="https://img.shields.io/badge/tests-1027-brightgreen" alt="Tests">
7
7
  </p>
8
8
 
9
9
  <h1 align="center">🐝 OpenCode Swarm</h1>
@@ -341,7 +341,7 @@ bunx opencode-swarm uninstall --clean
341
341
  - **Context injection budget** — `max_injection_tokens` config controls how much context is injected into system prompts. Priority-ordered: phase → task → decisions → agent context. Lower-priority items dropped when budget exhausted.
342
342
  - **Enhanced `/swarm agents`** — Agent count summary, `⚡ custom limits` indicator for profiled agents, guardrail profiles section.
343
343
  - **Packaging smoke tests** — CI-safe `dist/` validation (8 tests).
344
- - **208 new tests** — 876 total tests across 39 files (up from 668 in v4.6.0).
344
+ - **151 new tests** — 1027 total tests across 44 files (up from 876 in v4.6.0).
345
345
 
346
346
  ### v4.6.0 — Agent Guardrails
347
347
  - **Circuit breaker** — Two-layer protection against runaway agents. Soft warning at 50% of limits, hard block at 100%. Prevents infinite loops and runaway API costs.
@@ -422,6 +422,8 @@ All features are opt-in via configuration. See [Installation Guide](docs/install
422
422
  | `/swarm reset --confirm` | Clear swarm state files (with safety gate) |
423
423
  | `/swarm evidence [task]` | View evidence bundles for a task or all tasks |
424
424
  | `/swarm archive [--dry-run]` | Archive old evidence bundles with retention policy |
425
+ | `/swarm benchmark` | Run performance benchmarks and display metrics |
426
+ | `/swarm retrieve [id]` | Retrieve auto-summarized tool outputs by ID |
425
427
 
426
428
  ---
427
429
 
@@ -508,9 +510,7 @@ Override limits for specific agents that need more (or less) room:
508
510
 
509
511
  Profiles merge with base config — only specified fields are overridden.
510
512
 
511
- > **Built-in Architect Defaults:** The architect agent automatically receives higher limits
512
- > (600 tool calls, 90 min duration, 8 consecutive errors, 0.7 warning threshold) without any
513
- > configuration. These built-in defaults can be overridden via a `profiles.architect` entry.
513
+ > **Architect is exempt/unlimited by default:** The architect agent has no guardrail limits by default. To override, add a `profiles.architect` entry in your guardrails config.
514
514
 
515
515
  ### Disable Guardrails
516
516
 
@@ -564,7 +564,7 @@ bun test
564
564
  bun test tests/unit/config/schema.test.ts
565
565
  ```
566
566
 
567
- 876 unit tests across 39 files covering config, tools, agents, hooks, commands, state, guardrails, evidence, and plan schemas. Uses Bun's built-in test runner — zero additional test dependencies.
567
+ 1027 unit tests across 44 files covering config, tools, agents, hooks, commands, state, guardrails, evidence, and plan schemas. Uses Bun's built-in test runner — zero additional test dependencies.
568
568
 
569
569
  ## Troubleshooting
570
570
 
@@ -0,0 +1 @@
1
+ export declare function handleBenchmarkCommand(directory: string, args: string[]): Promise<string>;
@@ -1,6 +1,7 @@
1
1
  import type { AgentDefinition } from '../agents';
2
2
  export { handleAgentsCommand } from './agents';
3
3
  export { handleArchiveCommand } from './archive';
4
+ export { handleBenchmarkCommand } from './benchmark';
4
5
  export { handleConfigCommand } from './config';
5
6
  export { handleDiagnoseCommand } from './diagnose';
6
7
  export { handleEvidenceCommand } from './evidence';
@@ -8,6 +9,7 @@ export { handleExportCommand } from './export';
8
9
  export { handleHistoryCommand } from './history';
9
10
  export { handlePlanCommand } from './plan';
10
11
  export { handleResetCommand } from './reset';
12
+ export { handleRetrieveCommand } from './retrieve';
11
13
  export { handleStatusCommand } from './status';
12
14
  /**
13
15
  * Creates a command.execute.before handler for /swarm commands.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Handles the /swarm retrieve command.
3
+ * Loads full tool output from .swarm/summaries/{id}.json and returns it.
4
+ */
5
+ export declare function handleRetrieveCommand(directory: string, args: string[]): Promise<string>;
@@ -120,6 +120,14 @@ export declare const EvidenceConfigSchema: z.ZodObject<{
120
120
  auto_archive: z.ZodDefault<z.ZodBoolean>;
121
121
  }, z.core.$strip>;
122
122
  export type EvidenceConfig = z.infer<typeof EvidenceConfigSchema>;
123
+ export declare const SummaryConfigSchema: z.ZodObject<{
124
+ enabled: z.ZodDefault<z.ZodBoolean>;
125
+ threshold_bytes: z.ZodDefault<z.ZodNumber>;
126
+ max_summary_chars: z.ZodDefault<z.ZodNumber>;
127
+ max_stored_bytes: z.ZodDefault<z.ZodNumber>;
128
+ retention_days: z.ZodDefault<z.ZodNumber>;
129
+ }, z.core.$strip>;
130
+ export type SummaryConfig = z.infer<typeof SummaryConfigSchema>;
123
131
  export declare const GuardrailsProfileSchema: z.ZodObject<{
124
132
  max_tool_calls: z.ZodOptional<z.ZodNumber>;
125
133
  max_duration_minutes: z.ZodOptional<z.ZodNumber>;
@@ -262,6 +270,13 @@ export declare const PluginConfigSchema: z.ZodObject<{
262
270
  max_bundles: z.ZodDefault<z.ZodNumber>;
263
271
  auto_archive: z.ZodDefault<z.ZodBoolean>;
264
272
  }, z.core.$strip>>;
273
+ summaries: z.ZodOptional<z.ZodObject<{
274
+ enabled: z.ZodDefault<z.ZodBoolean>;
275
+ threshold_bytes: z.ZodDefault<z.ZodNumber>;
276
+ max_summary_chars: z.ZodDefault<z.ZodNumber>;
277
+ max_stored_bytes: z.ZodDefault<z.ZodNumber>;
278
+ retention_days: z.ZodDefault<z.ZodNumber>;
279
+ }, z.core.$strip>>;
265
280
  }, z.core.$strip>;
266
281
  export type PluginConfig = z.infer<typeof PluginConfigSchema>;
267
282
  export type { AgentName, PipelineAgentName, QAAgentName, } from './constants';
@@ -1,10 +1,11 @@
1
1
  export { createAgentActivityHooks } from './agent-activity';
2
2
  export { createCompactionCustomizerHook } from './compaction-customizer';
3
3
  export { createContextBudgetHandler } from './context-budget';
4
- export { createDelegationTrackerHook } from './delegation-tracker';
5
4
  export { createDelegationGateHook } from './delegation-gate';
5
+ export { createDelegationTrackerHook } from './delegation-tracker';
6
6
  export { extractCurrentPhase, extractCurrentPhaseFromPlan, extractCurrentTask, extractCurrentTaskFromPlan, extractDecisions, extractIncompleteTasks, extractIncompleteTasksFromPlan, extractPatterns, } from './extractors';
7
7
  export { createGuardrailsHooks } from './guardrails';
8
8
  export { createPipelineTrackerHook } from './pipeline-tracker';
9
9
  export { createSystemEnhancerHook } from './system-enhancer';
10
+ export { createToolSummarizerHook, resetSummaryIdCounter, } from './tool-summarizer';
10
11
  export { composeHandlers, estimateTokens, readSwarmFileAsync, safeHook, validateSwarmPath, } from './utils';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Tool Output Summarizer Hook
3
+ *
4
+ * Intercepts oversized tool outputs in tool.execute.after,
5
+ * stores the full content to .swarm/summaries/, and replaces
6
+ * the output with a compact summary containing a retrieval ID.
7
+ */
8
+ import type { SummaryConfig } from '../config/schema';
9
+ /**
10
+ * Reset the summary ID counter. Used for testing.
11
+ */
12
+ export declare function resetSummaryIdCounter(): void;
13
+ /**
14
+ * Creates a tool.execute.after hook that summarizes oversized tool outputs.
15
+ *
16
+ * @param config - Summary configuration including enabled, thresholds, and limits
17
+ * @param directory - Base directory for storing full outputs
18
+ * @returns Async hook function for tool.execute.after
19
+ */
20
+ export declare function createToolSummarizerHook(config: SummaryConfig, directory: string): (input: {
21
+ tool: string;
22
+ sessionID: string;
23
+ callID: string;
24
+ }, output: {
25
+ title: string;
26
+ output: string;
27
+ metadata: unknown;
28
+ }) => Promise<void>;
package/dist/index.js CHANGED
@@ -13623,6 +13623,13 @@ var EvidenceConfigSchema = exports_external.object({
13623
13623
  max_bundles: exports_external.number().min(10).max(1e4).default(1000),
13624
13624
  auto_archive: exports_external.boolean().default(false)
13625
13625
  });
13626
+ var SummaryConfigSchema = exports_external.object({
13627
+ enabled: exports_external.boolean().default(true),
13628
+ threshold_bytes: exports_external.number().min(1024).max(1048576).default(20480),
13629
+ max_summary_chars: exports_external.number().min(100).max(5000).default(1000),
13630
+ max_stored_bytes: exports_external.number().min(10240).max(104857600).default(10485760),
13631
+ retention_days: exports_external.number().min(1).max(365).default(7)
13632
+ });
13626
13633
  var GuardrailsProfileSchema = exports_external.object({
13627
13634
  max_tool_calls: exports_external.number().min(0).max(1000).optional(),
13628
13635
  max_duration_minutes: exports_external.number().min(0).max(480).optional(),
@@ -13716,7 +13723,8 @@ var PluginConfigSchema = exports_external.object({
13716
13723
  hooks: HooksConfigSchema.optional(),
13717
13724
  context_budget: ContextBudgetConfigSchema.optional(),
13718
13725
  guardrails: GuardrailsConfigSchema.optional(),
13719
- evidence: EvidenceConfigSchema.optional()
13726
+ evidence: EvidenceConfigSchema.optional(),
13727
+ summaries: SummaryConfigSchema.optional()
13720
13728
  });
13721
13729
 
13722
13730
  // src/config/loader.ts
@@ -15014,6 +15022,282 @@ async function handleArchiveCommand(directory, args) {
15014
15022
  `);
15015
15023
  }
15016
15024
 
15025
+ // src/state.ts
15026
+ var swarmState = {
15027
+ activeToolCalls: new Map,
15028
+ toolAggregates: new Map,
15029
+ activeAgent: new Map,
15030
+ delegationChains: new Map,
15031
+ pendingEvents: 0,
15032
+ agentSessions: new Map
15033
+ };
15034
+ function startAgentSession(sessionId, agentName, staleDurationMs = 7200000) {
15035
+ const now = Date.now();
15036
+ const staleIds = [];
15037
+ for (const [id, session] of swarmState.agentSessions) {
15038
+ if (now - session.lastToolCallTime > staleDurationMs) {
15039
+ staleIds.push(id);
15040
+ }
15041
+ }
15042
+ for (const id of staleIds) {
15043
+ swarmState.agentSessions.delete(id);
15044
+ }
15045
+ const sessionState = {
15046
+ agentName,
15047
+ startTime: now,
15048
+ lastToolCallTime: now,
15049
+ toolCallCount: 0,
15050
+ consecutiveErrors: 0,
15051
+ recentToolCalls: [],
15052
+ warningIssued: false,
15053
+ warningReason: "",
15054
+ hardLimitHit: false,
15055
+ lastSuccessTime: now,
15056
+ delegationActive: false
15057
+ };
15058
+ swarmState.agentSessions.set(sessionId, sessionState);
15059
+ }
15060
+ function getAgentSession(sessionId) {
15061
+ return swarmState.agentSessions.get(sessionId);
15062
+ }
15063
+ function ensureAgentSession(sessionId, agentName) {
15064
+ const now = Date.now();
15065
+ let session = swarmState.agentSessions.get(sessionId);
15066
+ if (session) {
15067
+ if (agentName && agentName !== session.agentName) {
15068
+ session.agentName = agentName;
15069
+ session.startTime = now;
15070
+ session.toolCallCount = 0;
15071
+ session.consecutiveErrors = 0;
15072
+ session.recentToolCalls = [];
15073
+ session.warningIssued = false;
15074
+ session.warningReason = "";
15075
+ session.hardLimitHit = false;
15076
+ session.lastSuccessTime = now;
15077
+ session.delegationActive = false;
15078
+ }
15079
+ session.lastToolCallTime = now;
15080
+ return session;
15081
+ }
15082
+ startAgentSession(sessionId, agentName ?? "unknown");
15083
+ session = swarmState.agentSessions.get(sessionId);
15084
+ if (!session) {
15085
+ throw new Error(`Failed to create guardrail session for ${sessionId}`);
15086
+ }
15087
+ return session;
15088
+ }
15089
+
15090
+ // src/commands/benchmark.ts
15091
+ var CI = {
15092
+ review_pass_rate: 70,
15093
+ test_pass_rate: 80,
15094
+ max_agent_error_rate: 20,
15095
+ max_hard_limit_hits: 1
15096
+ };
15097
+ async function handleBenchmarkCommand(directory, args) {
15098
+ let cumulative = args.includes("--cumulative");
15099
+ if (args.includes("--ci-gate"))
15100
+ cumulative = true;
15101
+ const mode = cumulative ? "cumulative" : "in-memory";
15102
+ const agentMap = new Map;
15103
+ for (const [, s] of swarmState.agentSessions) {
15104
+ const e = agentMap.get(s.agentName) || {
15105
+ toolCalls: 0,
15106
+ hardLimits: 0,
15107
+ warnings: 0
15108
+ };
15109
+ e.toolCalls += s.toolCallCount;
15110
+ if (s.hardLimitHit)
15111
+ e.hardLimits++;
15112
+ if (s.warningIssued)
15113
+ e.warnings++;
15114
+ agentMap.set(s.agentName, e);
15115
+ }
15116
+ const agentHealth = Array.from(agentMap.entries()).map(([a, v]) => ({
15117
+ agent: a,
15118
+ ...v
15119
+ }));
15120
+ const toolPerf = [];
15121
+ for (const [, a] of swarmState.toolAggregates) {
15122
+ const successRate = a.count ? a.successCount / a.count * 100 : 0;
15123
+ toolPerf.push({
15124
+ tool: a.tool,
15125
+ calls: a.count,
15126
+ successRate: Math.round(successRate * 10) / 10,
15127
+ avg: a.count ? Math.round(a.totalDuration / a.count) : 0
15128
+ });
15129
+ }
15130
+ toolPerf.sort((a, b) => b.calls - a.calls);
15131
+ let delegationCount = 0;
15132
+ for (const c of swarmState.delegationChains.values())
15133
+ delegationCount += c.length;
15134
+ let quality;
15135
+ if (cumulative) {
15136
+ let reviewPasses = 0, reviewFails = 0, testPasses = 0, testFails = 0, additions = 0, deletions = 0;
15137
+ for (const tid of await listEvidenceTaskIds(directory)) {
15138
+ const b = await loadEvidence(directory, tid);
15139
+ if (!b)
15140
+ continue;
15141
+ for (const e of b.entries) {
15142
+ if (e.type === "review") {
15143
+ if (e.verdict === "approved")
15144
+ reviewPasses++;
15145
+ else if (e.verdict === "rejected")
15146
+ reviewFails++;
15147
+ } else if (e.type === "test") {
15148
+ testPasses += e.tests_passed;
15149
+ testFails += e.tests_failed;
15150
+ } else if (e.type === "diff") {
15151
+ additions += e.additions;
15152
+ deletions += e.deletions;
15153
+ }
15154
+ }
15155
+ }
15156
+ const totalReviews = reviewPasses + reviewFails, totalTests = testPasses + testFails;
15157
+ quality = {
15158
+ reviewPassRate: totalReviews ? Math.round(reviewPasses / totalReviews * 1000) / 10 : null,
15159
+ testPassRate: totalTests ? Math.round(testPasses / totalTests * 1000) / 10 : null,
15160
+ totalReviews,
15161
+ testsPassed: testPasses,
15162
+ testsFailed: testFails,
15163
+ additions,
15164
+ deletions
15165
+ };
15166
+ }
15167
+ let ciGate;
15168
+ if (args.includes("--ci-gate")) {
15169
+ let totalCalls = 0, totalFailures = 0;
15170
+ for (const [, a] of swarmState.toolAggregates) {
15171
+ totalCalls += a.count;
15172
+ totalFailures += a.failureCount;
15173
+ }
15174
+ const agentErrorRate = totalCalls ? totalFailures / totalCalls * 100 : 0;
15175
+ let maxHardLimits = 0;
15176
+ for (const v of agentMap.values())
15177
+ if (v.hardLimits > maxHardLimits)
15178
+ maxHardLimits = v.hardLimits;
15179
+ const checks3 = [
15180
+ {
15181
+ name: "Review pass rate",
15182
+ value: quality?.reviewPassRate ?? 0,
15183
+ threshold: CI.review_pass_rate,
15184
+ operator: ">=",
15185
+ passed: (quality?.reviewPassRate ?? 0) >= CI.review_pass_rate
15186
+ },
15187
+ {
15188
+ name: "Test pass rate",
15189
+ value: quality?.testPassRate ?? 0,
15190
+ threshold: CI.test_pass_rate,
15191
+ operator: ">=",
15192
+ passed: (quality?.testPassRate ?? 0) >= CI.test_pass_rate
15193
+ },
15194
+ {
15195
+ name: "Agent error rate",
15196
+ value: Math.round(agentErrorRate * 10) / 10,
15197
+ threshold: CI.max_agent_error_rate,
15198
+ operator: "<=",
15199
+ passed: agentErrorRate <= CI.max_agent_error_rate
15200
+ },
15201
+ {
15202
+ name: "Hard limit hits",
15203
+ value: maxHardLimits,
15204
+ threshold: CI.max_hard_limit_hits,
15205
+ operator: "<=",
15206
+ passed: maxHardLimits <= CI.max_hard_limit_hits
15207
+ }
15208
+ ];
15209
+ ciGate = { passed: checks3.every((c) => c.passed), checks: checks3 };
15210
+ }
15211
+ const lines = [
15212
+ `## Swarm Benchmark (mode: ${mode})`,
15213
+ "",
15214
+ "### Agent Health"
15215
+ ];
15216
+ if (!agentHealth.length)
15217
+ lines.push("No agent sessions recorded");
15218
+ else
15219
+ for (const { agent, toolCalls, hardLimits, warnings } of agentHealth) {
15220
+ const parts = [`${toolCalls} tool calls`];
15221
+ if (warnings > 0)
15222
+ parts.push(`${warnings} warning${warnings > 1 ? "s" : ""}`);
15223
+ parts.push(hardLimits ? `${hardLimits} hard limit hit${hardLimits > 1 ? "s" : ""}` : "0 hard limits");
15224
+ lines.push(`- ${hardLimits ? "\u26A0\uFE0F" : "\u2705"} **${agent}**: ${parts.join(", ")}`);
15225
+ }
15226
+ lines.push("", "### Tool Performance");
15227
+ if (!toolPerf.length)
15228
+ lines.push("No tool data recorded");
15229
+ else {
15230
+ lines.push("| Tool | Calls | Success Rate | Avg Duration |", "|------|-------|-------------|-------------|");
15231
+ for (const { tool, calls, successRate, avg } of toolPerf)
15232
+ lines.push(`| ${tool} | ${calls} | ${successRate}% | ${avg}ms |`);
15233
+ }
15234
+ lines.push("", "### Delegations", delegationCount ? `Total: ${delegationCount} delegations` : "No delegations recorded", "");
15235
+ if (quality) {
15236
+ lines.push("### Quality Signals");
15237
+ if (!quality.totalReviews && !quality.testsPassed && !quality.additions)
15238
+ lines.push("No evidence data found");
15239
+ else {
15240
+ if (quality.reviewPassRate !== null)
15241
+ lines.push(`- Review pass rate: ${quality.reviewPassRate}% (${quality.totalReviews}) ${quality.reviewPassRate >= 70 ? "\u2705" : "\u274C"}`);
15242
+ else
15243
+ lines.push("- Review pass rate: N/A (no reviews)");
15244
+ if (quality.testPassRate !== null)
15245
+ lines.push(`- Test pass rate: ${quality.testPassRate}% (${quality.testsPassed}/${quality.testsPassed + quality.testsFailed}) ${quality.testPassRate >= 80 ? "\u2705" : "\u274C"}`);
15246
+ else
15247
+ lines.push("- Test pass rate: N/A (no tests)");
15248
+ lines.push(`- Code churn: +${quality.additions} / -${quality.deletions} lines`);
15249
+ }
15250
+ lines.push("");
15251
+ }
15252
+ if (ciGate) {
15253
+ lines.push("### CI Gate", ciGate.passed ? "\u2705 PASSED" : "\u274C FAILED");
15254
+ for (const c of ciGate.checks)
15255
+ lines.push(`- ${c.name}: ${c.value}% ${c.operator} ${c.threshold}% ${c.passed ? "\u2705" : "\u274C"}`);
15256
+ lines.push("");
15257
+ }
15258
+ const json2 = {
15259
+ mode,
15260
+ timestamp: new Date().toISOString(),
15261
+ agent_health: agentHealth.map((a) => ({
15262
+ agent: a.agent,
15263
+ tool_calls: a.toolCalls,
15264
+ hard_limit_hits: a.hardLimits,
15265
+ warnings: a.warnings
15266
+ })),
15267
+ tool_performance: toolPerf.map((t) => ({
15268
+ tool: t.tool,
15269
+ calls: t.calls,
15270
+ success_rate: t.successRate,
15271
+ avg_duration_ms: t.avg
15272
+ })),
15273
+ delegations: delegationCount
15274
+ };
15275
+ if (quality)
15276
+ json2.quality = {
15277
+ review_pass_rate: quality.reviewPassRate,
15278
+ test_pass_rate: quality.testPassRate,
15279
+ total_reviews: quality.totalReviews,
15280
+ total_tests_passed: quality.testsPassed,
15281
+ total_tests_failed: quality.testsFailed,
15282
+ additions: quality.additions,
15283
+ deletions: quality.deletions
15284
+ };
15285
+ if (ciGate)
15286
+ json2.ci_gate = {
15287
+ passed: ciGate.passed,
15288
+ checks: ciGate.checks.map((c) => ({
15289
+ name: c.name,
15290
+ value: c.value,
15291
+ threshold: c.threshold,
15292
+ operator: c.operator,
15293
+ passed: c.passed
15294
+ }))
15295
+ };
15296
+ lines.push("[BENCHMARK_JSON]", JSON.stringify(json2, null, 2), "[/BENCHMARK_JSON]");
15297
+ return lines.join(`
15298
+ `);
15299
+ }
15300
+
15017
15301
  // src/commands/config.ts
15018
15302
  import * as os2 from "os";
15019
15303
  import * as path4 from "path";
@@ -15724,6 +16008,17 @@ async function handleResetCommand(directory, args) {
15724
16008
  results.push(`- \u274C Failed to delete ${filename}`);
15725
16009
  }
15726
16010
  }
16011
+ try {
16012
+ const summariesPath = validateSwarmPath(directory, "summaries");
16013
+ if (fs2.existsSync(summariesPath)) {
16014
+ fs2.rmSync(summariesPath, { recursive: true, force: true });
16015
+ results.push("- \u2705 Deleted summaries/ directory");
16016
+ } else {
16017
+ results.push("- \u23ED\uFE0F summaries/ not found (skipped)");
16018
+ }
16019
+ } catch {
16020
+ results.push("- \u274C Failed to delete summaries/");
16021
+ }
15727
16022
  return [
15728
16023
  "## Swarm Reset Complete",
15729
16024
  "",
@@ -15734,6 +16029,112 @@ async function handleResetCommand(directory, args) {
15734
16029
  `);
15735
16030
  }
15736
16031
 
16032
+ // src/summaries/manager.ts
16033
+ import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync3 } from "fs";
16034
+ import * as path6 from "path";
16035
+ var SUMMARY_ID_REGEX = /^S\d+$/;
16036
+ function sanitizeSummaryId(id) {
16037
+ if (!id || id.length === 0) {
16038
+ throw new Error("Invalid summary ID: empty string");
16039
+ }
16040
+ if (/\0/.test(id)) {
16041
+ throw new Error("Invalid summary ID: contains null bytes");
16042
+ }
16043
+ for (let i = 0;i < id.length; i++) {
16044
+ if (id.charCodeAt(i) < 32) {
16045
+ throw new Error("Invalid summary ID: contains control characters");
16046
+ }
16047
+ }
16048
+ if (id.includes("..") || id.includes("../") || id.includes("..\\")) {
16049
+ throw new Error("Invalid summary ID: path traversal detected");
16050
+ }
16051
+ if (!SUMMARY_ID_REGEX.test(id)) {
16052
+ throw new Error(`Invalid summary ID: must match pattern ^S\\d+$, got "${id}"`);
16053
+ }
16054
+ return id;
16055
+ }
16056
+ async function storeSummary(directory, id, fullOutput, summaryText, maxStoredBytes) {
16057
+ const sanitizedId = sanitizeSummaryId(id);
16058
+ const outputBytes = Buffer.byteLength(fullOutput, "utf8");
16059
+ if (outputBytes > maxStoredBytes) {
16060
+ throw new Error(`Summary fullOutput size (${outputBytes} bytes) exceeds maximum (${maxStoredBytes} bytes)`);
16061
+ }
16062
+ const relativePath = path6.join("summaries", `${sanitizedId}.json`);
16063
+ const summaryPath = validateSwarmPath(directory, relativePath);
16064
+ const summaryDir = path6.dirname(summaryPath);
16065
+ const entry = {
16066
+ id: sanitizedId,
16067
+ summaryText,
16068
+ fullOutput,
16069
+ timestamp: Date.now(),
16070
+ originalBytes: outputBytes
16071
+ };
16072
+ const entryJson = JSON.stringify(entry);
16073
+ mkdirSync2(summaryDir, { recursive: true });
16074
+ const tempPath = path6.join(summaryDir, `${sanitizedId}.json.tmp.${Date.now()}.${process.pid}`);
16075
+ try {
16076
+ await Bun.write(tempPath, entryJson);
16077
+ renameSync2(tempPath, summaryPath);
16078
+ } catch (error49) {
16079
+ try {
16080
+ rmSync3(tempPath, { force: true });
16081
+ } catch {}
16082
+ throw error49;
16083
+ }
16084
+ }
16085
+ async function loadFullOutput(directory, id) {
16086
+ const sanitizedId = sanitizeSummaryId(id);
16087
+ const relativePath = path6.join("summaries", `${sanitizedId}.json`);
16088
+ validateSwarmPath(directory, relativePath);
16089
+ const content = await readSwarmFileAsync(directory, relativePath);
16090
+ if (content === null) {
16091
+ return null;
16092
+ }
16093
+ try {
16094
+ const parsed = JSON.parse(content);
16095
+ if (typeof parsed.fullOutput === "string") {
16096
+ return parsed.fullOutput;
16097
+ }
16098
+ warn(`Summary entry ${sanitizedId} missing valid fullOutput field`);
16099
+ return null;
16100
+ } catch (error49) {
16101
+ warn(`Summary entry validation failed for ${sanitizedId}: ${error49 instanceof Error ? error49.message : String(error49)}`);
16102
+ return null;
16103
+ }
16104
+ }
16105
+
16106
+ // src/commands/retrieve.ts
16107
+ async function handleRetrieveCommand(directory, args) {
16108
+ const summaryId = args[0];
16109
+ if (!summaryId) {
16110
+ return [
16111
+ "## Swarm Retrieve",
16112
+ "",
16113
+ "Usage: `/swarm retrieve <id>`",
16114
+ "",
16115
+ "Example: `/swarm retrieve S1`",
16116
+ "",
16117
+ "Retrieves the full output that was replaced by a summary."
16118
+ ].join(`
16119
+ `);
16120
+ }
16121
+ try {
16122
+ const fullOutput = await loadFullOutput(directory, summaryId);
16123
+ if (fullOutput === null) {
16124
+ return `## Summary Not Found
16125
+
16126
+ No stored output found for ID \`${summaryId}\`.
16127
+
16128
+ Use a valid summary ID (e.g., S1, S2, S3).`;
16129
+ }
16130
+ return fullOutput;
16131
+ } catch (error49) {
16132
+ return `## Retrieve Failed
16133
+
16134
+ ${error49 instanceof Error ? error49.message : String(error49)}`;
16135
+ }
16136
+ }
16137
+
15737
16138
  // src/hooks/extractors.ts
15738
16139
  function extractCurrentPhase(planContent) {
15739
16140
  if (!planContent) {
@@ -15982,8 +16383,10 @@ var HELP_TEXT = [
15982
16383
  "- `/swarm evidence [taskId]` \u2014 Show evidence bundles",
15983
16384
  "- `/swarm archive [--dry-run]` \u2014 Archive old evidence bundles",
15984
16385
  "- `/swarm diagnose` \u2014 Run health check on swarm state",
16386
+ "- `/swarm benchmark [--cumulative] [--ci-gate]` \u2014 Show performance metrics",
15985
16387
  "- `/swarm export` \u2014 Export plan and context as JSON",
15986
- "- `/swarm reset --confirm` \u2014 Clear swarm state files"
16388
+ "- `/swarm reset --confirm` \u2014 Clear swarm state files",
16389
+ "- `/swarm retrieve <id>` \u2014 Retrieve full output from a summary"
15987
16390
  ].join(`
15988
16391
  `);
15989
16392
  function createSwarmCommandHandler(directory, agents) {
@@ -16022,12 +16425,18 @@ function createSwarmCommandHandler(directory, agents) {
16022
16425
  case "diagnose":
16023
16426
  text = await handleDiagnoseCommand(directory, args);
16024
16427
  break;
16428
+ case "benchmark":
16429
+ text = await handleBenchmarkCommand(directory, args);
16430
+ break;
16025
16431
  case "export":
16026
16432
  text = await handleExportCommand(directory, args);
16027
16433
  break;
16028
16434
  case "reset":
16029
16435
  text = await handleResetCommand(directory, args);
16030
16436
  break;
16437
+ case "retrieve":
16438
+ text = await handleRetrieveCommand(directory, args);
16439
+ break;
16031
16440
  default:
16032
16441
  text = HELP_TEXT;
16033
16442
  break;
@@ -16038,71 +16447,6 @@ function createSwarmCommandHandler(directory, agents) {
16038
16447
  };
16039
16448
  }
16040
16449
 
16041
- // src/state.ts
16042
- var swarmState = {
16043
- activeToolCalls: new Map,
16044
- toolAggregates: new Map,
16045
- activeAgent: new Map,
16046
- delegationChains: new Map,
16047
- pendingEvents: 0,
16048
- agentSessions: new Map
16049
- };
16050
- function startAgentSession(sessionId, agentName, staleDurationMs = 7200000) {
16051
- const now = Date.now();
16052
- const staleIds = [];
16053
- for (const [id, session] of swarmState.agentSessions) {
16054
- if (now - session.lastToolCallTime > staleDurationMs) {
16055
- staleIds.push(id);
16056
- }
16057
- }
16058
- for (const id of staleIds) {
16059
- swarmState.agentSessions.delete(id);
16060
- }
16061
- const sessionState = {
16062
- agentName,
16063
- startTime: now,
16064
- lastToolCallTime: now,
16065
- toolCallCount: 0,
16066
- consecutiveErrors: 0,
16067
- recentToolCalls: [],
16068
- warningIssued: false,
16069
- warningReason: "",
16070
- hardLimitHit: false,
16071
- lastSuccessTime: now,
16072
- delegationActive: false
16073
- };
16074
- swarmState.agentSessions.set(sessionId, sessionState);
16075
- }
16076
- function getAgentSession(sessionId) {
16077
- return swarmState.agentSessions.get(sessionId);
16078
- }
16079
- function ensureAgentSession(sessionId, agentName) {
16080
- const now = Date.now();
16081
- let session = swarmState.agentSessions.get(sessionId);
16082
- if (session) {
16083
- if (agentName && agentName !== session.agentName) {
16084
- session.agentName = agentName;
16085
- session.startTime = now;
16086
- session.toolCallCount = 0;
16087
- session.consecutiveErrors = 0;
16088
- session.recentToolCalls = [];
16089
- session.warningIssued = false;
16090
- session.warningReason = "";
16091
- session.hardLimitHit = false;
16092
- session.lastSuccessTime = now;
16093
- session.delegationActive = false;
16094
- }
16095
- session.lastToolCallTime = now;
16096
- return session;
16097
- }
16098
- startAgentSession(sessionId, agentName ?? "unknown");
16099
- session = swarmState.agentSessions.get(sessionId);
16100
- if (!session) {
16101
- throw new Error(`Failed to create guardrail session for ${sessionId}`);
16102
- }
16103
- return session;
16104
- }
16105
-
16106
16450
  // src/hooks/agent-activity.ts
16107
16451
  function createAgentActivityHooks(config2, directory) {
16108
16452
  if (config2.hooks?.agent_activity === false) {
@@ -16171,8 +16515,8 @@ async function doFlush(directory) {
16171
16515
  const activitySection = renderActivitySection();
16172
16516
  const updated = replaceOrAppendSection(existing, "## Agent Activity", activitySection);
16173
16517
  const flushedCount = swarmState.pendingEvents;
16174
- const path6 = `${directory}/.swarm/context.md`;
16175
- await Bun.write(path6, updated);
16518
+ const path7 = `${directory}/.swarm/context.md`;
16519
+ await Bun.write(path7, updated);
16176
16520
  swarmState.pendingEvents = Math.max(0, swarmState.pendingEvents - flushedCount);
16177
16521
  } catch (error49) {
16178
16522
  warn("Agent activity flush failed:", error49);
@@ -16322,36 +16666,6 @@ function createContextBudgetHandler(config2) {
16322
16666
  }
16323
16667
  };
16324
16668
  }
16325
- // src/hooks/delegation-tracker.ts
16326
- function createDelegationTrackerHook(config2) {
16327
- return async (input, _output) => {
16328
- if (!input.agent || input.agent === "") {
16329
- const session2 = swarmState.agentSessions.get(input.sessionID);
16330
- if (session2) {
16331
- session2.delegationActive = false;
16332
- }
16333
- return;
16334
- }
16335
- const agentName = input.agent;
16336
- const previousAgent = swarmState.activeAgent.get(input.sessionID);
16337
- swarmState.activeAgent.set(input.sessionID, agentName);
16338
- const session = ensureAgentSession(input.sessionID, agentName);
16339
- session.delegationActive = true;
16340
- if (config2.hooks?.delegation_tracker === true && previousAgent && previousAgent !== agentName) {
16341
- const entry = {
16342
- from: previousAgent,
16343
- to: agentName,
16344
- timestamp: Date.now()
16345
- };
16346
- if (!swarmState.delegationChains.has(input.sessionID)) {
16347
- swarmState.delegationChains.set(input.sessionID, []);
16348
- }
16349
- const chain = swarmState.delegationChains.get(input.sessionID);
16350
- chain?.push(entry);
16351
- swarmState.pendingEvents++;
16352
- }
16353
- };
16354
- }
16355
16669
  // src/hooks/delegation-gate.ts
16356
16670
  function createDelegationGateHook(config2) {
16357
16671
  const enabled = config2.hooks?.delegation_gate !== false;
@@ -16416,6 +16730,38 @@ Split into smaller, atomic tasks for better results.]`;
16416
16730
  ${originalText}`;
16417
16731
  };
16418
16732
  }
16733
+ // src/hooks/delegation-tracker.ts
16734
+ function createDelegationTrackerHook(config2) {
16735
+ return async (input, _output) => {
16736
+ if (!input.agent || input.agent === "") {
16737
+ const session2 = swarmState.agentSessions.get(input.sessionID);
16738
+ if (session2) {
16739
+ session2.delegationActive = false;
16740
+ }
16741
+ swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
16742
+ ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
16743
+ return;
16744
+ }
16745
+ const agentName = input.agent;
16746
+ const previousAgent = swarmState.activeAgent.get(input.sessionID);
16747
+ swarmState.activeAgent.set(input.sessionID, agentName);
16748
+ const session = ensureAgentSession(input.sessionID, agentName);
16749
+ session.delegationActive = true;
16750
+ if (config2.hooks?.delegation_tracker === true && previousAgent && previousAgent !== agentName) {
16751
+ const entry = {
16752
+ from: previousAgent,
16753
+ to: agentName,
16754
+ timestamp: Date.now()
16755
+ };
16756
+ if (!swarmState.delegationChains.has(input.sessionID)) {
16757
+ swarmState.delegationChains.set(input.sessionID, []);
16758
+ }
16759
+ const chain = swarmState.delegationChains.get(input.sessionID);
16760
+ chain?.push(entry);
16761
+ swarmState.pendingEvents++;
16762
+ }
16763
+ };
16764
+ }
16419
16765
  // src/hooks/guardrails.ts
16420
16766
  function createGuardrailsHooks(config2) {
16421
16767
  if (config2.enabled === false) {
@@ -16432,8 +16778,19 @@ function createGuardrailsHooks(config2) {
16432
16778
  if (strippedAgent === ORCHESTRATOR_NAME) {
16433
16779
  return;
16434
16780
  }
16781
+ const existingSession = swarmState.agentSessions.get(input.sessionID);
16782
+ if (existingSession) {
16783
+ const sessionAgent = stripKnownSwarmPrefix(existingSession.agentName);
16784
+ if (sessionAgent === ORCHESTRATOR_NAME) {
16785
+ return;
16786
+ }
16787
+ }
16435
16788
  const agentName = swarmState.activeAgent.get(input.sessionID);
16436
16789
  const session = ensureAgentSession(input.sessionID, agentName);
16790
+ const resolvedName = stripKnownSwarmPrefix(session.agentName);
16791
+ if (resolvedName === ORCHESTRATOR_NAME) {
16792
+ return;
16793
+ }
16437
16794
  const agentConfig = resolveGuardrailsConfig(config2, session.agentName);
16438
16795
  if (session.hardLimitHit) {
16439
16796
  throw new Error("\uD83D\uDED1 CIRCUIT BREAKER: Agent blocked. Hard limit was previously triggered. Stop making tool calls and return your progress summary.");
@@ -16762,6 +17119,7 @@ function createSystemEnhancerHook(config2, directory) {
16762
17119
  }
16763
17120
  }
16764
17121
  }
17122
+ tryInject("[SWARM HINT] Large tool outputs may be auto-summarized. Use /swarm retrieve <id> to get the full content if needed.");
16765
17123
  return;
16766
17124
  }
16767
17125
  const userScoringConfig = config2.context_budget?.scoring;
@@ -16887,6 +17245,154 @@ ${activitySection}`;
16887
17245
  }
16888
17246
  return contextSummary;
16889
17247
  }
17248
+ // src/summaries/summarizer.ts
17249
+ var HYSTERESIS_FACTOR = 1.25;
17250
+ function detectContentType(output, toolName) {
17251
+ const trimmed = output.trim();
17252
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
17253
+ try {
17254
+ JSON.parse(trimmed);
17255
+ return "json";
17256
+ } catch {}
17257
+ }
17258
+ const codeToolNames = ["read", "cat", "grep", "bash"];
17259
+ const lowerToolName = toolName.toLowerCase();
17260
+ const toolSegments = lowerToolName.split(/[.\-_/]/);
17261
+ if (codeToolNames.some((name) => toolSegments.includes(name))) {
17262
+ return "code";
17263
+ }
17264
+ const codePatterns = [
17265
+ "function ",
17266
+ "const ",
17267
+ "import ",
17268
+ "export ",
17269
+ "class ",
17270
+ "def ",
17271
+ "return ",
17272
+ "=>"
17273
+ ];
17274
+ const startsWithShebang = trimmed.startsWith("#!");
17275
+ if (codePatterns.some((pattern) => output.includes(pattern)) || startsWithShebang) {
17276
+ return "code";
17277
+ }
17278
+ const sampleSize = Math.min(1000, output.length);
17279
+ let nonPrintableCount = 0;
17280
+ for (let i = 0;i < sampleSize; i++) {
17281
+ const charCode = output.charCodeAt(i);
17282
+ if (charCode < 32 && charCode !== 9 && charCode !== 10 && charCode !== 13) {
17283
+ nonPrintableCount++;
17284
+ }
17285
+ }
17286
+ if (sampleSize > 0 && nonPrintableCount / sampleSize > 0.1) {
17287
+ return "binary";
17288
+ }
17289
+ return "text";
17290
+ }
17291
+ function shouldSummarize(output, thresholdBytes) {
17292
+ const byteLength = Buffer.byteLength(output, "utf8");
17293
+ return byteLength >= thresholdBytes * HYSTERESIS_FACTOR;
17294
+ }
17295
+ function formatBytes(bytes) {
17296
+ const units = ["B", "KB", "MB", "GB"];
17297
+ let unitIndex = 0;
17298
+ let size = bytes;
17299
+ while (size >= 1024 && unitIndex < units.length - 1) {
17300
+ size /= 1024;
17301
+ unitIndex++;
17302
+ }
17303
+ const formatted = unitIndex === 0 ? size.toString() : size.toFixed(1);
17304
+ return `${formatted} ${units[unitIndex]}`;
17305
+ }
17306
+ function createSummary(output, toolName, summaryId, maxSummaryChars) {
17307
+ const contentType = detectContentType(output, toolName);
17308
+ const lineCount = output.split(`
17309
+ `).length;
17310
+ const byteSize = Buffer.byteLength(output, "utf8");
17311
+ const formattedSize = formatBytes(byteSize);
17312
+ const headerLine = `[SUMMARY ${summaryId}] ${formattedSize} | ${contentType} | ${lineCount} lines`;
17313
+ const footerLine = `\u2192 Use /swarm retrieve ${summaryId} for full content`;
17314
+ const overhead = headerLine.length + 1 + footerLine.length + 1;
17315
+ const maxPreviewChars = maxSummaryChars - overhead;
17316
+ let preview;
17317
+ switch (contentType) {
17318
+ case "json": {
17319
+ try {
17320
+ const parsed = JSON.parse(output.trim());
17321
+ if (Array.isArray(parsed)) {
17322
+ preview = `[ ${parsed.length} items ]`;
17323
+ } else if (typeof parsed === "object" && parsed !== null) {
17324
+ const keys = Object.keys(parsed).slice(0, 3);
17325
+ preview = `{ ${keys.join(", ")}${Object.keys(parsed).length > 3 ? ", ..." : ""} }`;
17326
+ } else {
17327
+ const lines = output.split(`
17328
+ `).filter((line) => line.trim().length > 0).slice(0, 3);
17329
+ preview = lines.join(`
17330
+ `);
17331
+ }
17332
+ } catch {
17333
+ const lines = output.split(`
17334
+ `).filter((line) => line.trim().length > 0).slice(0, 3);
17335
+ preview = lines.join(`
17336
+ `);
17337
+ }
17338
+ break;
17339
+ }
17340
+ case "code": {
17341
+ const lines = output.split(`
17342
+ `).filter((line) => line.trim().length > 0).slice(0, 5);
17343
+ preview = lines.join(`
17344
+ `);
17345
+ break;
17346
+ }
17347
+ case "text": {
17348
+ const lines = output.split(`
17349
+ `).filter((line) => line.trim().length > 0).slice(0, 5);
17350
+ preview = lines.join(`
17351
+ `);
17352
+ break;
17353
+ }
17354
+ case "binary": {
17355
+ preview = `[Binary content - ${formattedSize}]`;
17356
+ break;
17357
+ }
17358
+ default: {
17359
+ const lines = output.split(`
17360
+ `).filter((line) => line.trim().length > 0).slice(0, 5);
17361
+ preview = lines.join(`
17362
+ `);
17363
+ }
17364
+ }
17365
+ if (preview.length > maxPreviewChars) {
17366
+ preview = preview.substring(0, maxPreviewChars - 3) + "...";
17367
+ }
17368
+ return `${headerLine}
17369
+ ${preview}
17370
+ ${footerLine}`;
17371
+ }
17372
+
17373
+ // src/hooks/tool-summarizer.ts
17374
+ var nextSummaryId = 1;
17375
+ function createToolSummarizerHook(config2, directory) {
17376
+ if (config2.enabled === false) {
17377
+ return async () => {};
17378
+ }
17379
+ return async (input, output) => {
17380
+ if (typeof output.output !== "string" || output.output.length === 0) {
17381
+ return;
17382
+ }
17383
+ if (!shouldSummarize(output.output, config2.threshold_bytes)) {
17384
+ return;
17385
+ }
17386
+ const summaryId = `S${nextSummaryId++}`;
17387
+ const summaryText = createSummary(output.output, input.tool, summaryId, config2.max_summary_chars);
17388
+ try {
17389
+ await storeSummary(directory, summaryId, output.output, summaryText, config2.max_stored_bytes);
17390
+ output.output = summaryText;
17391
+ } catch (error49) {
17392
+ warn(`Tool output summarization failed for ${summaryId}: ${error49 instanceof Error ? error49.message : String(error49)}`);
17393
+ }
17394
+ };
17395
+ }
16890
17396
  // node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js
16891
17397
  var exports_external2 = {};
16892
17398
  __export(exports_external2, {
@@ -17616,10 +18122,10 @@ function mergeDefs2(...defs) {
17616
18122
  function cloneDef2(schema) {
17617
18123
  return mergeDefs2(schema._zod.def);
17618
18124
  }
17619
- function getElementAtPath2(obj, path6) {
17620
- if (!path6)
18125
+ function getElementAtPath2(obj, path7) {
18126
+ if (!path7)
17621
18127
  return obj;
17622
- return path6.reduce((acc, key) => acc?.[key], obj);
18128
+ return path7.reduce((acc, key) => acc?.[key], obj);
17623
18129
  }
17624
18130
  function promiseAllObject2(promisesObj) {
17625
18131
  const keys = Object.keys(promisesObj);
@@ -17978,11 +18484,11 @@ function aborted2(x, startIndex = 0) {
17978
18484
  }
17979
18485
  return false;
17980
18486
  }
17981
- function prefixIssues2(path6, issues) {
18487
+ function prefixIssues2(path7, issues) {
17982
18488
  return issues.map((iss) => {
17983
18489
  var _a2;
17984
18490
  (_a2 = iss).path ?? (_a2.path = []);
17985
- iss.path.unshift(path6);
18491
+ iss.path.unshift(path7);
17986
18492
  return iss;
17987
18493
  });
17988
18494
  }
@@ -18150,7 +18656,7 @@ function treeifyError2(error49, _mapper) {
18150
18656
  return issue3.message;
18151
18657
  };
18152
18658
  const result = { errors: [] };
18153
- const processError = (error50, path6 = []) => {
18659
+ const processError = (error50, path7 = []) => {
18154
18660
  var _a2, _b;
18155
18661
  for (const issue3 of error50.issues) {
18156
18662
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -18160,7 +18666,7 @@ function treeifyError2(error49, _mapper) {
18160
18666
  } else if (issue3.code === "invalid_element") {
18161
18667
  processError({ issues: issue3.issues }, issue3.path);
18162
18668
  } else {
18163
- const fullpath = [...path6, ...issue3.path];
18669
+ const fullpath = [...path7, ...issue3.path];
18164
18670
  if (fullpath.length === 0) {
18165
18671
  result.errors.push(mapper(issue3));
18166
18672
  continue;
@@ -18192,8 +18698,8 @@ function treeifyError2(error49, _mapper) {
18192
18698
  }
18193
18699
  function toDotPath2(_path) {
18194
18700
  const segs = [];
18195
- const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
18196
- for (const seg of path6) {
18701
+ const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
18702
+ for (const seg of path7) {
18197
18703
  if (typeof seg === "number")
18198
18704
  segs.push(`[${seg}]`);
18199
18705
  else if (typeof seg === "symbol")
@@ -29389,7 +29895,7 @@ Use these as DOMAIN values when delegating to @sme.`;
29389
29895
  });
29390
29896
  // src/tools/file-extractor.ts
29391
29897
  import * as fs3 from "fs";
29392
- import * as path6 from "path";
29898
+ import * as path7 from "path";
29393
29899
  var EXT_MAP = {
29394
29900
  python: ".py",
29395
29901
  py: ".py",
@@ -29467,12 +29973,12 @@ var extract_code_blocks = tool({
29467
29973
  if (prefix) {
29468
29974
  filename = `${prefix}_${filename}`;
29469
29975
  }
29470
- let filepath = path6.join(targetDir, filename);
29471
- const base = path6.basename(filepath, path6.extname(filepath));
29472
- const ext = path6.extname(filepath);
29976
+ let filepath = path7.join(targetDir, filename);
29977
+ const base = path7.basename(filepath, path7.extname(filepath));
29978
+ const ext = path7.extname(filepath);
29473
29979
  let counter = 1;
29474
29980
  while (fs3.existsSync(filepath)) {
29475
- filepath = path6.join(targetDir, `${base}_${counter}${ext}`);
29981
+ filepath = path7.join(targetDir, `${base}_${counter}${ext}`);
29476
29982
  counter++;
29477
29983
  }
29478
29984
  try {
@@ -29596,6 +30102,8 @@ var OpenCodeSwarm = async (ctx) => {
29596
30102
  const delegationGateHandler = createDelegationGateHook(config3);
29597
30103
  const guardrailsConfig = GuardrailsConfigSchema.parse(config3.guardrails ?? {});
29598
30104
  const guardrailsHooks = createGuardrailsHooks(guardrailsConfig);
30105
+ const summaryConfig = SummaryConfigSchema.parse(config3.summaries ?? {});
30106
+ const toolSummarizerHook = createToolSummarizerHook(summaryConfig, ctx.directory);
29599
30107
  log("Plugin initialized", {
29600
30108
  directory: ctx.directory,
29601
30109
  maxIterations: config3.max_iterations,
@@ -29609,7 +30117,8 @@ var OpenCodeSwarm = async (ctx) => {
29609
30117
  commands: true,
29610
30118
  agentActivity: config3.hooks?.agent_activity !== false,
29611
30119
  delegationTracker: config3.hooks?.delegation_tracker === true,
29612
- guardrails: guardrailsConfig.enabled
30120
+ guardrails: guardrailsConfig.enabled,
30121
+ toolSummarizer: summaryConfig.enabled
29613
30122
  }
29614
30123
  });
29615
30124
  return {
@@ -29653,14 +30162,20 @@ var OpenCodeSwarm = async (ctx) => {
29653
30162
  }
29654
30163
  const session = swarmState.agentSessions.get(input.sessionID);
29655
30164
  const activeAgent = swarmState.activeAgent.get(input.sessionID);
29656
- if (session && activeAgent && activeAgent !== ORCHESTRATOR_NAME && session.delegationActive === false) {
29657
- swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
29658
- ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
30165
+ if (session && activeAgent && activeAgent !== ORCHESTRATOR_NAME) {
30166
+ const stripActive = stripKnownSwarmPrefix(activeAgent);
30167
+ if (stripActive !== ORCHESTRATOR_NAME) {
30168
+ const staleDelegation = !session.delegationActive || Date.now() - session.lastToolCallTime > 60000;
30169
+ if (staleDelegation) {
30170
+ swarmState.activeAgent.set(input.sessionID, ORCHESTRATOR_NAME);
30171
+ ensureAgentSession(input.sessionID, ORCHESTRATOR_NAME);
30172
+ }
30173
+ }
29659
30174
  }
29660
30175
  await guardrailsHooks.toolBefore(input, output);
29661
30176
  await safeHook(activityHooks.toolBefore)(input, output);
29662
30177
  },
29663
- "tool.execute.after": composeHandlers(activityHooks.toolAfter, guardrailsHooks.toolAfter),
30178
+ "tool.execute.after": composeHandlers(activityHooks.toolAfter, guardrailsHooks.toolAfter, toolSummarizerHook),
29664
30179
  "chat.message": safeHook(delegationHandler)
29665
30180
  };
29666
30181
  };
@@ -0,0 +1,2 @@
1
+ export { cleanupSummaries, listSummaries, loadFullOutput, storeSummary, } from './manager';
2
+ export { createSummary, detectContentType, HYSTERESIS_FACTOR, shouldSummarize, } from './summarizer';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Validate and sanitize summary ID.
3
+ * Must match regex ^S\d+$ (e.g., S1, S2, S99)
4
+ * Rejects: empty string, null bytes, control characters, path traversal, non-matching patterns
5
+ * @throws Error with descriptive message on failure
6
+ */
7
+ export declare function sanitizeSummaryId(id: string): string;
8
+ /**
9
+ * Store a summary entry to .swarm/summaries/{id}.json.
10
+ * Performs atomic write via temp file + rename.
11
+ * @throws Error if summary ID is invalid or size limit would be exceeded
12
+ */
13
+ export declare function storeSummary(directory: string, id: string, fullOutput: string, summaryText: string, maxStoredBytes: number): Promise<void>;
14
+ /**
15
+ * Load fullOutput from a summary entry.
16
+ * Returns null if file doesn't exist or validation fails.
17
+ */
18
+ export declare function loadFullOutput(directory: string, id: string): Promise<string | null>;
19
+ /**
20
+ * List all summary IDs that have summary entries.
21
+ * Returns sorted array of valid summary IDs.
22
+ * Returns empty array if summaries directory doesn't exist.
23
+ */
24
+ export declare function listSummaries(directory: string): Promise<string[]>;
25
+ /**
26
+ * Delete summaries older than retentionDays.
27
+ * Returns array of deleted summary IDs.
28
+ */
29
+ export declare function cleanupSummaries(directory: string, retentionDays: number): Promise<string[]>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Summarization engine for tool outputs.
3
+ * Provides content type detection, summarization decision logic, and structured summary creation.
4
+ */
5
+ /**
6
+ * Hysteresis factor to prevent churn for outputs near the threshold.
7
+ * An output must be 25% larger than the threshold to be summarized.
8
+ */
9
+ export declare const HYSTERESIS_FACTOR = 1.25;
10
+ /**
11
+ * Content type classification for tool outputs.
12
+ */
13
+ type ContentType = 'json' | 'code' | 'text' | 'binary';
14
+ /**
15
+ * Heuristic-based content type detection.
16
+ * @param output - The tool output string to analyze
17
+ * @param toolName - The name of the tool that produced the output
18
+ * @returns The detected content type: 'json', 'code', 'text', or 'binary'
19
+ */
20
+ export declare function detectContentType(output: string, toolName: string): ContentType;
21
+ /**
22
+ * Determines whether output should be summarized based on size and hysteresis.
23
+ * Uses hysteresis to prevent repeated summarization decisions for outputs near the threshold.
24
+ * @param output - The tool output string to check
25
+ * @param thresholdBytes - The threshold in bytes
26
+ * @returns true if the output should be summarized
27
+ */
28
+ export declare function shouldSummarize(output: string, thresholdBytes: number): boolean;
29
+ /**
30
+ * Creates a structured summary string from tool output.
31
+ * @param output - The full tool output string
32
+ * @param toolName - The name of the tool that produced the output
33
+ * @param summaryId - Unique identifier for this summary
34
+ * @param maxSummaryChars - Maximum characters allowed for the preview
35
+ * @returns Formatted summary string
36
+ */
37
+ export declare function createSummary(output: string, toolName: string, summaryId: string, maxSummaryChars: number): string;
38
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "5.1.1",
3
+ "version": "5.1.4",
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",