opencode-swarm 6.1.1 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <p align="center">
2
- <img src="https://img.shields.io/badge/version-6.1.1-blue" alt="Version">
2
+ <img src="https://img.shields.io/badge/version-6.2.0-blue" alt="Version">
3
3
  <img src="https://img.shields.io/badge/license-MIT-green" alt="License">
4
4
  <img src="https://img.shields.io/badge/opencode-plugin-purple" alt="OpenCode Plugin">
5
5
  <img src="https://img.shields.io/badge/agents-9-orange" alt="Agents">
6
- <img src="https://img.shields.io/badge/tests-1280-brightgreen" alt="Tests">
6
+ <img src="https://img.shields.io/badge/tests-1391-brightgreen" alt="Tests">
7
7
  </p>
8
8
 
9
9
  <h1 align="center">🐝 OpenCode Swarm</h1>
@@ -233,6 +233,21 @@ Current Phase: 2
233
233
 
234
234
  **Start a new session tomorrow?** The Architect reads these files and picks up exactly where you left off.
235
235
 
236
+ ### Evidence Types
237
+
238
+ Each task in `.swarm/evidence/` contains structured evidence bundles with typed entries:
239
+
240
+ | Type | Purpose | Key Fields |
241
+ |------|---------|------------|
242
+ | `review` | Code review verdict | `verdict`, `risk`, `issues[]` |
243
+ | `test` | Test run results | `verdict`, `tests_passed`, `tests_failed`, `coverage` |
244
+ | `diff` | Git diff summary | `files_changed[]`, `additions`, `deletions` |
245
+ | `approval` | Stakeholder sign-off | `approver`, `notes` |
246
+ | `note` | General observations | `content` |
247
+ | `retrospective` | Phase metrics & lessons | `phase_number`, `total_tool_calls`, `coder_revisions`, `reviewer_rejections`, `test_failures`, `security_findings`, `task_count`, `task_complexity`, `top_rejection_reasons[]`, `lessons_learned[]` |
248
+
249
+ **Retrospective Evidence** (v6.2.0+): After each phase completes, the architect writes a retrospective evidence bundle capturing what went well and what didn't. The system enhancer injects the most recent retrospective as a `[SWARM RETROSPECTIVE]` hint at the start of the next phase, enabling continuous improvement across phases.
250
+
236
251
  ---
237
252
 
238
253
  ## Heterogeneous Models = Better Code
@@ -343,6 +358,18 @@ bunx opencode-swarm uninstall --clean
343
358
 
344
359
  ## What's New
345
360
 
361
+ ### v6.2.0 — System Intelligence
362
+ - **Retrospective evidence** — New evidence type that captures phase metrics (tool calls, revisions, rejections, test failures, security findings) and lessons learned. Architect writes it after each phase; system enhancer injects the most recent one as a `[SWARM RETROSPECTIVE]` hint for the next phase, enabling continuous improvement across phases.
363
+ - **Soft compaction advisory** — System enhancer injects a `[SWARM HINT]` when the architect's tool-call count crosses configurable thresholds (default 50/75/100/125/150). A `lastCompactionHint` guard prevents re-injection at the same threshold. Configurable via `compaction_advisory` block.
364
+ - **Coverage reporting** — Test engineer now reports line/branch/function coverage percentages and flags files below 70%. Architect uses this in Phase 5 step 5d to request additional test passes when coverage is insufficient.
365
+ - **111 new tests** — 1391 total tests across 62+ files (up from 1280 in v6.1.2).
366
+
367
+ ### v6.1.2 — Guardrails Remediation
368
+ - **Fail-safe config validation** — Config validation failures now disable guardrails as a safety precaution (previously Zod defaults could silently re-enable them).
369
+ - **Architect exemption fix** — Architect/orchestrator sessions can no longer inherit 30-minute base limits during delegation race conditions.
370
+ - **Explicit disable always wins** — `guardrails.enabled: false` in config is now always honored, even when the config was loaded from file.
371
+ - **Internal map synchronization** — `startAgentSession()` now keeps `activeAgent` and `agentSessions` maps in sync for consistent state tracking.
372
+
346
373
  ### v6.1.1 — Security Fix & Tech Debt
347
374
  - **Security hardening (`_loadedFromFile`)** — Fixed a critical vulnerability where an internal loader flag could be injected via JSON config to bypass guardrails. The flag is now purely internal and no longer part of the public schema.
348
375
  - **TOCTOU protection** — Added atomic-style content checks in the config loader to prevent race conditions during file reads.
@@ -587,6 +614,22 @@ Control whether contract change detection triggers impact analysis:
587
614
  }
588
615
  ```
589
616
 
617
+ ### Compaction Advisory
618
+
619
+ Control when the system hints about context compaction thresholds:
620
+
621
+ ```jsonc
622
+ {
623
+ "compaction_advisory": {
624
+ "enabled": true, // default: true
625
+ "thresholds": [50, 75, 100, 125, 150], // tool-call counts that trigger hints
626
+ "message": "Large context may benefit from compaction" // custom message
627
+ }
628
+ }
629
+ ```
630
+
631
+ When the architect's tool-call count crosses a threshold, the system enhancer injects a `[SWARM HINT]` suggesting context management. Each threshold fires only once per session (tracked via `lastCompactionHint`).
632
+
590
633
  > **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.
591
634
 
592
635
  ### Per-Invocation Budgets
@@ -653,7 +696,7 @@ bun test
653
696
  bun test tests/unit/config/schema.test.ts
654
697
  ```
655
698
 
656
- 1280 tests across 57+ files covering config, tools, agents, hooks, commands, state, guardrails, evidence, plan schemas, circuit breaker race conditions, invocation windows, multi-invocation isolation, security categories, review/integration schemas, and diff tool. Uses Bun's built-in test runner — zero additional test dependencies.
699
+ 1391 tests across 62+ files covering config, tools, agents, hooks, commands, state, guardrails, evidence, plan schemas, circuit breaker race conditions, invocation windows, multi-invocation isolation, security categories, review/integration schemas, and diff tool. Uses Bun's built-in test runner — zero additional test dependencies.
657
700
 
658
701
  ## Troubleshooting
659
702
 
@@ -8,6 +8,7 @@ export declare const EvidenceTypeSchema: z.ZodEnum<{
8
8
  diff: "diff";
9
9
  approval: "approval";
10
10
  note: "note";
11
+ retrospective: "retrospective";
11
12
  }>;
12
13
  export type EvidenceType = z.infer<typeof EvidenceTypeSchema>;
13
14
  export declare const EvidenceVerdictSchema: z.ZodEnum<{
@@ -26,6 +27,7 @@ export declare const BaseEvidenceSchema: z.ZodObject<{
26
27
  diff: "diff";
27
28
  approval: "approval";
28
29
  note: "note";
30
+ retrospective: "retrospective";
29
31
  }>;
30
32
  timestamp: z.ZodString;
31
33
  agent: z.ZodString;
@@ -147,6 +149,38 @@ export declare const NoteEvidenceSchema: z.ZodObject<{
147
149
  type: z.ZodLiteral<"note">;
148
150
  }, z.core.$strip>;
149
151
  export type NoteEvidence = z.infer<typeof NoteEvidenceSchema>;
152
+ export declare const RetrospectiveEvidenceSchema: z.ZodObject<{
153
+ task_id: z.ZodString;
154
+ timestamp: z.ZodString;
155
+ agent: z.ZodString;
156
+ verdict: z.ZodEnum<{
157
+ pass: "pass";
158
+ fail: "fail";
159
+ approved: "approved";
160
+ rejected: "rejected";
161
+ info: "info";
162
+ }>;
163
+ summary: z.ZodString;
164
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
165
+ type: z.ZodLiteral<"retrospective">;
166
+ phase_number: z.ZodNumber;
167
+ total_tool_calls: z.ZodNumber;
168
+ coder_revisions: z.ZodNumber;
169
+ reviewer_rejections: z.ZodNumber;
170
+ test_failures: z.ZodNumber;
171
+ security_findings: z.ZodNumber;
172
+ integration_issues: z.ZodNumber;
173
+ task_count: z.ZodNumber;
174
+ task_complexity: z.ZodEnum<{
175
+ trivial: "trivial";
176
+ simple: "simple";
177
+ moderate: "moderate";
178
+ complex: "complex";
179
+ }>;
180
+ top_rejection_reasons: z.ZodDefault<z.ZodArray<z.ZodString>>;
181
+ lessons_learned: z.ZodDefault<z.ZodArray<z.ZodString>>;
182
+ }, z.core.$strip>;
183
+ export type RetrospectiveEvidence = z.infer<typeof RetrospectiveEvidenceSchema>;
150
184
  export declare const EvidenceSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
151
185
  task_id: z.ZodString;
152
186
  timestamp: z.ZodString;
@@ -244,6 +278,36 @@ export declare const EvidenceSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
244
278
  summary: z.ZodString;
245
279
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
246
280
  type: z.ZodLiteral<"note">;
281
+ }, z.core.$strip>, z.ZodObject<{
282
+ task_id: z.ZodString;
283
+ timestamp: z.ZodString;
284
+ agent: z.ZodString;
285
+ verdict: z.ZodEnum<{
286
+ pass: "pass";
287
+ fail: "fail";
288
+ approved: "approved";
289
+ rejected: "rejected";
290
+ info: "info";
291
+ }>;
292
+ summary: z.ZodString;
293
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
294
+ type: z.ZodLiteral<"retrospective">;
295
+ phase_number: z.ZodNumber;
296
+ total_tool_calls: z.ZodNumber;
297
+ coder_revisions: z.ZodNumber;
298
+ reviewer_rejections: z.ZodNumber;
299
+ test_failures: z.ZodNumber;
300
+ security_findings: z.ZodNumber;
301
+ integration_issues: z.ZodNumber;
302
+ task_count: z.ZodNumber;
303
+ task_complexity: z.ZodEnum<{
304
+ trivial: "trivial";
305
+ simple: "simple";
306
+ moderate: "moderate";
307
+ complex: "complex";
308
+ }>;
309
+ top_rejection_reasons: z.ZodDefault<z.ZodArray<z.ZodString>>;
310
+ lessons_learned: z.ZodDefault<z.ZodArray<z.ZodString>>;
247
311
  }, z.core.$strip>], "type">;
248
312
  export type Evidence = z.infer<typeof EvidenceSchema>;
249
313
  export declare const EvidenceBundleSchema: z.ZodObject<{
@@ -346,6 +410,36 @@ export declare const EvidenceBundleSchema: z.ZodObject<{
346
410
  summary: z.ZodString;
347
411
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
348
412
  type: z.ZodLiteral<"note">;
413
+ }, z.core.$strip>, z.ZodObject<{
414
+ task_id: z.ZodString;
415
+ timestamp: z.ZodString;
416
+ agent: z.ZodString;
417
+ verdict: z.ZodEnum<{
418
+ pass: "pass";
419
+ fail: "fail";
420
+ approved: "approved";
421
+ rejected: "rejected";
422
+ info: "info";
423
+ }>;
424
+ summary: z.ZodString;
425
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
426
+ type: z.ZodLiteral<"retrospective">;
427
+ phase_number: z.ZodNumber;
428
+ total_tool_calls: z.ZodNumber;
429
+ coder_revisions: z.ZodNumber;
430
+ reviewer_rejections: z.ZodNumber;
431
+ test_failures: z.ZodNumber;
432
+ security_findings: z.ZodNumber;
433
+ integration_issues: z.ZodNumber;
434
+ task_count: z.ZodNumber;
435
+ task_complexity: z.ZodEnum<{
436
+ trivial: "trivial";
437
+ simple: "simple";
438
+ moderate: "moderate";
439
+ complex: "complex";
440
+ }>;
441
+ top_rejection_reasons: z.ZodDefault<z.ZodArray<z.ZodString>>;
442
+ lessons_learned: z.ZodDefault<z.ZodArray<z.ZodString>>;
349
443
  }, z.core.$strip>], "type">>>;
350
444
  created_at: z.ZodString;
351
445
  updated_at: z.ZodString;
@@ -148,6 +148,12 @@ export declare const UIReviewConfigSchema: z.ZodObject<{
148
148
  trigger_keywords: z.ZodDefault<z.ZodArray<z.ZodString>>;
149
149
  }, z.core.$strip>;
150
150
  export type UIReviewConfig = z.infer<typeof UIReviewConfigSchema>;
151
+ export declare const CompactionAdvisoryConfigSchema: z.ZodObject<{
152
+ enabled: z.ZodDefault<z.ZodBoolean>;
153
+ thresholds: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
154
+ message: z.ZodDefault<z.ZodString>;
155
+ }, z.core.$strip>;
156
+ export type CompactionAdvisoryConfig = z.infer<typeof CompactionAdvisoryConfigSchema>;
151
157
  export declare const GuardrailsProfileSchema: z.ZodObject<{
152
158
  max_tool_calls: z.ZodOptional<z.ZodNumber>;
153
159
  max_duration_minutes: z.ZodOptional<z.ZodNumber>;
@@ -318,6 +324,11 @@ export declare const PluginConfigSchema: z.ZodObject<{
318
324
  trigger_paths: z.ZodDefault<z.ZodArray<z.ZodString>>;
319
325
  trigger_keywords: z.ZodDefault<z.ZodArray<z.ZodString>>;
320
326
  }, z.core.$strip>>;
327
+ compaction_advisory: z.ZodOptional<z.ZodObject<{
328
+ enabled: z.ZodDefault<z.ZodBoolean>;
329
+ thresholds: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
330
+ message: z.ZodDefault<z.ZodString>;
331
+ }, z.core.$strip>>;
321
332
  }, z.core.$strip>;
322
333
  export type PluginConfig = z.infer<typeof PluginConfigSchema>;
323
334
  export type { AgentName, PipelineAgentName, QAAgentName, } from './constants';
package/dist/index.js CHANGED
@@ -1,20 +1,5 @@
1
1
  // @bun
2
- var __create = Object.create;
3
- var __getProtoOf = Object.getPrototypeOf;
4
2
  var __defProp = Object.defineProperty;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __toESM = (mod, isNodeMode, target) => {
8
- target = mod != null ? __create(__getProtoOf(mod)) : {};
9
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
- for (let key of __getOwnPropNames(mod))
11
- if (!__hasOwnProp.call(to, key))
12
- __defProp(to, key, {
13
- get: () => mod[key],
14
- enumerable: true
15
- });
16
- return to;
17
- };
18
3
  var __export = (target, all) => {
19
4
  for (var name in all)
20
5
  __defProp(target, name, {
@@ -13644,7 +13629,8 @@ var EvidenceTypeSchema = exports_external.enum([
13644
13629
  "test",
13645
13630
  "diff",
13646
13631
  "approval",
13647
- "note"
13632
+ "note",
13633
+ "retrospective"
13648
13634
  ]);
13649
13635
  var EvidenceVerdictSchema = exports_external.enum([
13650
13636
  "pass",
@@ -13695,12 +13681,27 @@ var ApprovalEvidenceSchema = BaseEvidenceSchema.extend({
13695
13681
  var NoteEvidenceSchema = BaseEvidenceSchema.extend({
13696
13682
  type: exports_external.literal("note")
13697
13683
  });
13684
+ var RetrospectiveEvidenceSchema = BaseEvidenceSchema.extend({
13685
+ type: exports_external.literal("retrospective"),
13686
+ phase_number: exports_external.number().int().min(0),
13687
+ total_tool_calls: exports_external.number().int().min(0),
13688
+ coder_revisions: exports_external.number().int().min(0),
13689
+ reviewer_rejections: exports_external.number().int().min(0),
13690
+ test_failures: exports_external.number().int().min(0),
13691
+ security_findings: exports_external.number().int().min(0),
13692
+ integration_issues: exports_external.number().int().min(0),
13693
+ task_count: exports_external.number().int().min(1),
13694
+ task_complexity: exports_external.enum(["trivial", "simple", "moderate", "complex"]),
13695
+ top_rejection_reasons: exports_external.array(exports_external.string()).default([]),
13696
+ lessons_learned: exports_external.array(exports_external.string()).max(5).default([])
13697
+ });
13698
13698
  var EvidenceSchema = exports_external.discriminatedUnion("type", [
13699
13699
  ReviewEvidenceSchema,
13700
13700
  TestEvidenceSchema,
13701
13701
  DiffEvidenceSchema,
13702
13702
  ApprovalEvidenceSchema,
13703
- NoteEvidenceSchema
13703
+ NoteEvidenceSchema,
13704
+ RetrospectiveEvidenceSchema
13704
13705
  ]);
13705
13706
  var EvidenceBundleSchema = exports_external.object({
13706
13707
  schema_version: exports_external.literal("1.0.0"),
@@ -13836,6 +13837,11 @@ var UIReviewConfigSchema = exports_external.object({
13836
13837
  "profile page"
13837
13838
  ])
13838
13839
  });
13840
+ var CompactionAdvisoryConfigSchema = exports_external.object({
13841
+ enabled: exports_external.boolean().default(true),
13842
+ thresholds: exports_external.array(exports_external.number().int().min(10).max(500)).default([50, 75, 100, 125, 150]),
13843
+ message: exports_external.string().default("[SWARM HINT] Session has ${totalToolCalls} tool calls. Consider compacting at next phase boundary to maintain context quality.")
13844
+ });
13839
13845
  var GuardrailsProfileSchema = exports_external.object({
13840
13846
  max_tool_calls: exports_external.number().min(0).max(1000).optional(),
13841
13847
  max_duration_minutes: exports_external.number().min(0).max(480).optional(),
@@ -13949,7 +13955,8 @@ var PluginConfigSchema = exports_external.object({
13949
13955
  review_passes: ReviewPassesConfigSchema.optional(),
13950
13956
  integration_analysis: IntegrationAnalysisConfigSchema.optional(),
13951
13957
  docs: DocsConfigSchema.optional(),
13952
- ui_review: UIReviewConfigSchema.optional()
13958
+ ui_review: UIReviewConfigSchema.optional(),
13959
+ compaction_advisory: CompactionAdvisoryConfigSchema.optional()
13953
13960
  });
13954
13961
 
13955
13962
  // src/config/loader.ts
@@ -14010,7 +14017,9 @@ function loadPluginConfig(directory) {
14010
14017
  console.warn("[opencode-swarm] Merged config validation failed:");
14011
14018
  console.warn(result.error.format());
14012
14019
  console.warn("[opencode-swarm] \u26A0\uFE0F Guardrails will be DISABLED as a safety precaution. Fix the config file to restore normal operation.");
14013
- return PluginConfigSchema.parse({});
14020
+ return PluginConfigSchema.parse({
14021
+ guardrails: { enabled: false }
14022
+ });
14014
14023
  }
14015
14024
  return result.data;
14016
14025
  }
@@ -14130,6 +14139,7 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
14130
14139
  - Target file is in: pages/, components/, views/, screens/, ui/, layouts/
14131
14140
  If triggered: delegate to {{AGENT_PREFIX}}designer FIRST to produce a code scaffold. Then pass the scaffold to {{AGENT_PREFIX}}coder as INPUT alongside the task. The coder implements the TODOs in the scaffold without changing component structure or accessibility attributes.
14132
14141
  If not triggered: delegate directly to {{AGENT_PREFIX}}coder as normal.
14142
+ 10. **RETROSPECTIVE TRACKING**: At the end of every phase, record phase metrics in .swarm/context.md under "## Phase Metrics" and write a retrospective evidence entry via the evidence manager. Track: phase_number, total_tool_calls, coder_revisions, reviewer_rejections, test_failures, security_findings, integration_issues, task_count, task_complexity, top_rejection_reasons, lessons_learned (max 5). Reset Phase Metrics to 0 after writing.
14133
14143
 
14134
14144
  ## AGENTS
14135
14145
 
@@ -14294,7 +14304,8 @@ For each task (respecting dependencies):
14294
14304
  5e. Security gate: if file matches security globs or content has security keywords \u2192 {{AGENT_PREFIX}}reviewer security-only. REJECTED \u2192 coder retry.
14295
14305
  5f. {{AGENT_PREFIX}}test_engineer - Verification tests. FAIL \u2192 coder retry from 5d.
14296
14306
  5g. {{AGENT_PREFIX}}test_engineer - Adversarial tests. FAIL \u2192 coder retry from 5d.
14297
- 5h. Update plan.md [x], proceed to next task.
14307
+ 5h. COVERAGE CHECK: If test_engineer reports coverage < 70% \u2192 delegate {{AGENT_PREFIX}}test_engineer for an additional test pass targeting uncovered paths. This is a soft guideline; use judgment for trivial tasks.
14308
+ 5i. Update plan.md [x], proceed to next task.
14298
14309
 
14299
14310
  ### Phase 6: Phase Complete
14300
14311
  1. {{AGENT_PREFIX}}explorer - Rescan
@@ -14303,8 +14314,9 @@ For each task (respecting dependencies):
14303
14314
  - Summary of what was added/modified/removed
14304
14315
  - List of doc files that may need updating (README.md, CONTRIBUTING.md, docs/)
14305
14316
  3. Update context.md
14306
- 4. Summarize to user
14307
- 5. Ask: "Ready for Phase [N+1]?"
14317
+ 4. Write retrospective evidence: record phase_number, total_tool_calls, coder_revisions, reviewer_rejections, test_failures, security_findings, integration_issues, task_count, task_complexity, top_rejection_reasons, lessons_learned to .swarm/evidence/ via the evidence manager. Reset Phase Metrics in context.md to 0.
14318
+ 5. Summarize to user
14319
+ 6. Ask: "Ready for Phase [N+1]?"
14308
14320
 
14309
14321
  ### Blockers
14310
14322
  Mark [BLOCKED] in plan.md, skip to next unblocked task, inform user.
@@ -14904,7 +14916,13 @@ OUTPUT FORMAT:
14904
14916
  VERDICT: PASS | FAIL
14905
14917
  TESTS: [total count] tests, [pass count] passed, [fail count] failed
14906
14918
  FAILURES: [list of failed test names + error messages, if any]
14907
- COVERAGE: [areas covered]`;
14919
+ COVERAGE: [areas covered]
14920
+
14921
+ COVERAGE REPORTING:
14922
+ - After running tests, report the line/branch coverage percentage if the test runner provides it.
14923
+ - Format: COVERAGE_PCT: [N]% (or "N/A" if not available)
14924
+ - If COVERAGE_PCT < 70%, add a note: "COVERAGE_WARNING: Below 70% threshold \u2014 consider additional test cases for uncovered paths."
14925
+ - The architect uses this to decide whether to request an additional test pass (Rule 10 / Phase 5 step 5h).`;
14908
14926
  function createTestEngineerAgent(model, customPrompt, customAppendPrompt) {
14909
14927
  let prompt = TEST_ENGINEER_PROMPT;
14910
14928
  if (customPrompt) {
@@ -15447,9 +15465,11 @@ function startAgentSession(sessionId, agentName, staleDurationMs = 7200000) {
15447
15465
  delegationActive: false,
15448
15466
  activeInvocationId: 0,
15449
15467
  lastInvocationIdByAgent: {},
15450
- windows: {}
15468
+ windows: {},
15469
+ lastCompactionHint: 0
15451
15470
  };
15452
15471
  swarmState.agentSessions.set(sessionId, sessionState);
15472
+ swarmState.activeAgent.set(sessionId, agentName);
15453
15473
  }
15454
15474
  function ensureAgentSession(sessionId, agentName) {
15455
15475
  const now = Date.now();
@@ -15470,6 +15490,9 @@ function ensureAgentSession(sessionId, agentName) {
15470
15490
  session.lastInvocationIdByAgent = {};
15471
15491
  session.windows = {};
15472
15492
  }
15493
+ if (session.lastCompactionHint === undefined) {
15494
+ session.lastCompactionHint = 0;
15495
+ }
15473
15496
  session.lastToolCallTime = now;
15474
15497
  return session;
15475
15498
  }
@@ -17268,7 +17291,7 @@ function createGuardrailsHooks(config2) {
17268
17291
  return;
17269
17292
  }
17270
17293
  }
17271
- const agentName = swarmState.activeAgent.get(input.sessionID);
17294
+ const agentName = swarmState.activeAgent.get(input.sessionID) ?? ORCHESTRATOR_NAME;
17272
17295
  const session = ensureAgentSession(input.sessionID, agentName);
17273
17296
  const resolvedName = stripKnownSwarmPrefix(session.agentName);
17274
17297
  if (resolvedName === ORCHESTRATOR_NAME) {
@@ -17491,6 +17514,10 @@ ${originalText}`;
17491
17514
  })
17492
17515
  };
17493
17516
  }
17517
+ // src/hooks/system-enhancer.ts
17518
+ import * as fs3 from "fs";
17519
+ import * as path7 from "path";
17520
+
17494
17521
  // src/hooks/context-scoring.ts
17495
17522
  function calculateAgeFactor(ageHours, config2) {
17496
17523
  if (ageHours <= 0) {
@@ -17626,6 +17653,66 @@ function createSystemEnhancerHook(config2, directory) {
17626
17653
  if (config2.docs?.enabled === false) {
17627
17654
  tryInject("[SWARM CONFIG] Docs agent is DISABLED. Skip docs delegation in Phase 6.");
17628
17655
  }
17656
+ const sessionId_retro = _input.sessionID;
17657
+ const activeAgent_retro = swarmState.activeAgent.get(sessionId_retro ?? "");
17658
+ const isArchitect = !activeAgent_retro || stripKnownSwarmPrefix(activeAgent_retro) === "architect";
17659
+ if (isArchitect) {
17660
+ try {
17661
+ const evidenceDir = path7.join(directory, ".swarm", "evidence");
17662
+ if (fs3.existsSync(evidenceDir)) {
17663
+ const files = fs3.readdirSync(evidenceDir).filter((f) => f.endsWith(".json")).sort().reverse();
17664
+ for (const file2 of files.slice(0, 5)) {
17665
+ const content = JSON.parse(fs3.readFileSync(path7.join(evidenceDir, file2), "utf-8"));
17666
+ if (content.type === "retrospective") {
17667
+ const retro = content;
17668
+ const hints = [];
17669
+ if (retro.reviewer_rejections > 2) {
17670
+ hints.push(`Phase ${retro.phase_number} had ${retro.reviewer_rejections} reviewer rejections.`);
17671
+ }
17672
+ if (retro.top_rejection_reasons.length > 0) {
17673
+ hints.push(`Common rejection reasons: ${retro.top_rejection_reasons.join(", ")}.`);
17674
+ }
17675
+ if (retro.lessons_learned.length > 0) {
17676
+ hints.push(`Lessons: ${retro.lessons_learned.join("; ")}.`);
17677
+ }
17678
+ if (hints.length > 0) {
17679
+ const retroHint = `[SWARM RETROSPECTIVE] From Phase ${retro.phase_number}: ${hints.join(" ")}`;
17680
+ if (retroHint.length <= 800) {
17681
+ tryInject(retroHint);
17682
+ } else {
17683
+ tryInject(retroHint.substring(0, 800) + "...");
17684
+ }
17685
+ }
17686
+ break;
17687
+ }
17688
+ }
17689
+ }
17690
+ } catch {}
17691
+ const compactionConfig = config2.compaction_advisory;
17692
+ if (compactionConfig?.enabled !== false && sessionId_retro) {
17693
+ const session = swarmState.agentSessions.get(sessionId_retro);
17694
+ if (session) {
17695
+ const totalToolCalls = Array.from(swarmState.toolAggregates.values()).reduce((sum, agg) => sum + agg.count, 0);
17696
+ const thresholds = compactionConfig?.thresholds ?? [
17697
+ 50,
17698
+ 75,
17699
+ 100,
17700
+ 125,
17701
+ 150
17702
+ ];
17703
+ const lastHint = session.lastCompactionHint || 0;
17704
+ for (const threshold of thresholds) {
17705
+ if (totalToolCalls >= threshold && lastHint < threshold) {
17706
+ const messageTemplate = compactionConfig?.message ?? "[SWARM HINT] Session has ${totalToolCalls} tool calls. Consider compacting at next phase boundary to maintain context quality.";
17707
+ const message = messageTemplate.replace("${totalToolCalls}", String(totalToolCalls));
17708
+ tryInject(message);
17709
+ session.lastCompactionHint = threshold;
17710
+ break;
17711
+ }
17712
+ }
17713
+ }
17714
+ }
17715
+ }
17629
17716
  return;
17630
17717
  }
17631
17718
  const userScoringConfig = config2.context_budget?.scoring;
@@ -17749,6 +17836,77 @@ function createSystemEnhancerHook(config2, directory) {
17749
17836
  metadata: { contentType: "prose" }
17750
17837
  });
17751
17838
  }
17839
+ const sessionId_retro_b = _input.sessionID;
17840
+ const activeAgent_retro_b = swarmState.activeAgent.get(sessionId_retro_b ?? "");
17841
+ const isArchitect_b = !activeAgent_retro_b || stripKnownSwarmPrefix(activeAgent_retro_b) === "architect";
17842
+ if (isArchitect_b) {
17843
+ try {
17844
+ const evidenceDir_b = path7.join(directory, ".swarm", "evidence");
17845
+ if (fs3.existsSync(evidenceDir_b)) {
17846
+ const files_b = fs3.readdirSync(evidenceDir_b).filter((f) => f.endsWith(".json")).sort().reverse();
17847
+ for (const file2 of files_b.slice(0, 5)) {
17848
+ const content_b = JSON.parse(fs3.readFileSync(path7.join(evidenceDir_b, file2), "utf-8"));
17849
+ if (content_b.type === "retrospective") {
17850
+ const retro_b = content_b;
17851
+ const hints_b = [];
17852
+ if (retro_b.reviewer_rejections > 2) {
17853
+ hints_b.push(`Phase ${retro_b.phase_number} had ${retro_b.reviewer_rejections} reviewer rejections.`);
17854
+ }
17855
+ if (retro_b.top_rejection_reasons.length > 0) {
17856
+ hints_b.push(`Common rejection reasons: ${retro_b.top_rejection_reasons.join(", ")}.`);
17857
+ }
17858
+ if (retro_b.lessons_learned.length > 0) {
17859
+ hints_b.push(`Lessons: ${retro_b.lessons_learned.join("; ")}.`);
17860
+ }
17861
+ if (hints_b.length > 0) {
17862
+ const retroHint_b = `[SWARM RETROSPECTIVE] From Phase ${retro_b.phase_number}: ${hints_b.join(" ")}`;
17863
+ const retroText = retroHint_b.length <= 800 ? retroHint_b : retroHint_b.substring(0, 800) + "...";
17864
+ candidates.push({
17865
+ id: `candidate-${idCounter++}`,
17866
+ kind: "phase",
17867
+ text: retroText,
17868
+ tokens: estimateTokens(retroText),
17869
+ priority: 2,
17870
+ metadata: { contentType: "prose" }
17871
+ });
17872
+ }
17873
+ break;
17874
+ }
17875
+ }
17876
+ }
17877
+ } catch {}
17878
+ const compactionConfig_b = config2.compaction_advisory;
17879
+ if (compactionConfig_b?.enabled !== false && sessionId_retro_b) {
17880
+ const session_b = swarmState.agentSessions.get(sessionId_retro_b);
17881
+ if (session_b) {
17882
+ const totalToolCalls_b = Array.from(swarmState.toolAggregates.values()).reduce((sum, agg) => sum + agg.count, 0);
17883
+ const thresholds_b = compactionConfig_b?.thresholds ?? [
17884
+ 50,
17885
+ 75,
17886
+ 100,
17887
+ 125,
17888
+ 150
17889
+ ];
17890
+ const lastHint_b = session_b.lastCompactionHint || 0;
17891
+ for (const threshold of thresholds_b) {
17892
+ if (totalToolCalls_b >= threshold && lastHint_b < threshold) {
17893
+ const messageTemplate_b = compactionConfig_b?.message ?? "[SWARM HINT] Session has ${totalToolCalls} tool calls. Consider compacting at next phase boundary to maintain context quality.";
17894
+ const compactionText = messageTemplate_b.replace("${totalToolCalls}", String(totalToolCalls_b));
17895
+ candidates.push({
17896
+ id: `candidate-${idCounter++}`,
17897
+ kind: "phase",
17898
+ text: compactionText,
17899
+ tokens: estimateTokens(compactionText),
17900
+ priority: 1,
17901
+ metadata: { contentType: "prose" }
17902
+ });
17903
+ session_b.lastCompactionHint = threshold;
17904
+ break;
17905
+ }
17906
+ }
17907
+ }
17908
+ }
17909
+ }
17752
17910
  const ranked = rankCandidates(candidates, effectiveConfig);
17753
17911
  for (const candidate of ranked) {
17754
17912
  if (injectedTokens + candidate.tokens > maxInjectionTokens) {
@@ -18675,10 +18833,10 @@ function mergeDefs2(...defs) {
18675
18833
  function cloneDef2(schema) {
18676
18834
  return mergeDefs2(schema._zod.def);
18677
18835
  }
18678
- function getElementAtPath2(obj, path7) {
18679
- if (!path7)
18836
+ function getElementAtPath2(obj, path8) {
18837
+ if (!path8)
18680
18838
  return obj;
18681
- return path7.reduce((acc, key) => acc?.[key], obj);
18839
+ return path8.reduce((acc, key) => acc?.[key], obj);
18682
18840
  }
18683
18841
  function promiseAllObject2(promisesObj) {
18684
18842
  const keys = Object.keys(promisesObj);
@@ -19037,11 +19195,11 @@ function aborted2(x, startIndex = 0) {
19037
19195
  }
19038
19196
  return false;
19039
19197
  }
19040
- function prefixIssues2(path7, issues) {
19198
+ function prefixIssues2(path8, issues) {
19041
19199
  return issues.map((iss) => {
19042
19200
  var _a2;
19043
19201
  (_a2 = iss).path ?? (_a2.path = []);
19044
- iss.path.unshift(path7);
19202
+ iss.path.unshift(path8);
19045
19203
  return iss;
19046
19204
  });
19047
19205
  }
@@ -19209,7 +19367,7 @@ function treeifyError2(error49, _mapper) {
19209
19367
  return issue3.message;
19210
19368
  };
19211
19369
  const result = { errors: [] };
19212
- const processError = (error50, path7 = []) => {
19370
+ const processError = (error50, path8 = []) => {
19213
19371
  var _a2, _b;
19214
19372
  for (const issue3 of error50.issues) {
19215
19373
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -19219,7 +19377,7 @@ function treeifyError2(error49, _mapper) {
19219
19377
  } else if (issue3.code === "invalid_element") {
19220
19378
  processError({ issues: issue3.issues }, issue3.path);
19221
19379
  } else {
19222
- const fullpath = [...path7, ...issue3.path];
19380
+ const fullpath = [...path8, ...issue3.path];
19223
19381
  if (fullpath.length === 0) {
19224
19382
  result.errors.push(mapper(issue3));
19225
19383
  continue;
@@ -19251,8 +19409,8 @@ function treeifyError2(error49, _mapper) {
19251
19409
  }
19252
19410
  function toDotPath2(_path) {
19253
19411
  const segs = [];
19254
- const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
19255
- for (const seg of path7) {
19412
+ const path8 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
19413
+ for (const seg of path8) {
19256
19414
  if (typeof seg === "number")
19257
19415
  segs.push(`[${seg}]`);
19258
19416
  else if (typeof seg === "symbol")
@@ -30292,14 +30450,14 @@ function validateBase(base) {
30292
30450
  function validatePaths(paths) {
30293
30451
  if (!paths)
30294
30452
  return null;
30295
- for (const path7 of paths) {
30296
- if (!path7 || path7.length === 0) {
30453
+ for (const path8 of paths) {
30454
+ if (!path8 || path8.length === 0) {
30297
30455
  return "empty path not allowed";
30298
30456
  }
30299
- if (path7.length > MAX_PATH_LENGTH) {
30457
+ if (path8.length > MAX_PATH_LENGTH) {
30300
30458
  return `path exceeds maximum length of ${MAX_PATH_LENGTH}`;
30301
30459
  }
30302
- if (SHELL_METACHARACTERS.test(path7)) {
30460
+ if (SHELL_METACHARACTERS.test(path8)) {
30303
30461
  return "path contains shell metacharacters";
30304
30462
  }
30305
30463
  }
@@ -30362,8 +30520,8 @@ var diff = tool({
30362
30520
  if (parts.length >= 3) {
30363
30521
  const additions = parseInt(parts[0]) || 0;
30364
30522
  const deletions = parseInt(parts[1]) || 0;
30365
- const path7 = parts[2];
30366
- files.push({ path: path7, additions, deletions });
30523
+ const path8 = parts[2];
30524
+ files.push({ path: path8, additions, deletions });
30367
30525
  }
30368
30526
  }
30369
30527
  const contractChanges = [];
@@ -30589,8 +30747,8 @@ Use these as DOMAIN values when delegating to @sme.`;
30589
30747
  }
30590
30748
  });
30591
30749
  // src/tools/file-extractor.ts
30592
- import * as fs3 from "fs";
30593
- import * as path7 from "path";
30750
+ import * as fs4 from "fs";
30751
+ import * as path8 from "path";
30594
30752
  var EXT_MAP = {
30595
30753
  python: ".py",
30596
30754
  py: ".py",
@@ -30652,8 +30810,8 @@ var extract_code_blocks = tool({
30652
30810
  execute: async (args) => {
30653
30811
  const { content, output_dir, prefix } = args;
30654
30812
  const targetDir = output_dir || process.cwd();
30655
- if (!fs3.existsSync(targetDir)) {
30656
- fs3.mkdirSync(targetDir, { recursive: true });
30813
+ if (!fs4.existsSync(targetDir)) {
30814
+ fs4.mkdirSync(targetDir, { recursive: true });
30657
30815
  }
30658
30816
  const pattern = /```(\w*)\n([\s\S]*?)```/g;
30659
30817
  const matches = [...content.matchAll(pattern)];
@@ -30668,16 +30826,16 @@ var extract_code_blocks = tool({
30668
30826
  if (prefix) {
30669
30827
  filename = `${prefix}_${filename}`;
30670
30828
  }
30671
- let filepath = path7.join(targetDir, filename);
30672
- const base = path7.basename(filepath, path7.extname(filepath));
30673
- const ext = path7.extname(filepath);
30829
+ let filepath = path8.join(targetDir, filename);
30830
+ const base = path8.basename(filepath, path8.extname(filepath));
30831
+ const ext = path8.extname(filepath);
30674
30832
  let counter = 1;
30675
- while (fs3.existsSync(filepath)) {
30676
- filepath = path7.join(targetDir, `${base}_${counter}${ext}`);
30833
+ while (fs4.existsSync(filepath)) {
30834
+ filepath = path8.join(targetDir, `${base}_${counter}${ext}`);
30677
30835
  counter++;
30678
30836
  }
30679
30837
  try {
30680
- fs3.writeFileSync(filepath, code.trim(), "utf-8");
30838
+ fs4.writeFileSync(filepath, code.trim(), "utf-8");
30681
30839
  savedFiles.push(filepath);
30682
30840
  } catch (error93) {
30683
30841
  errors5.push(`Failed to save ${filename}: ${error93 instanceof Error ? error93.message : String(error93)}`);
@@ -30824,7 +30982,7 @@ var OpenCodeSwarm = async (ctx) => {
30824
30982
  const commandHandler = createSwarmCommandHandler(ctx.directory, Object.fromEntries(agentDefinitions.map((agent) => [agent.name, agent])));
30825
30983
  const activityHooks = createAgentActivityHooks(config3, ctx.directory);
30826
30984
  const delegationGateHandler = createDelegationGateHook(config3);
30827
- const guardrailsFallback = loadedFromFile ? config3.guardrails ?? {} : { ...config3.guardrails, enabled: false };
30985
+ const guardrailsFallback = config3.guardrails?.enabled === false ? { ...config3.guardrails, enabled: false } : loadedFromFile ? config3.guardrails ?? {} : { ...config3.guardrails, enabled: false };
30828
30986
  const guardrailsConfig = GuardrailsConfigSchema.parse(guardrailsFallback);
30829
30987
  const delegationHandler = createDelegationTrackerHook(config3, guardrailsConfig.enabled);
30830
30988
  const guardrailsHooks = createGuardrailsHooks(guardrailsConfig);
package/dist/state.d.ts CHANGED
@@ -53,6 +53,8 @@ export interface AgentSessionState {
53
53
  lastInvocationIdByAgent: Record<string, number>;
54
54
  /** Active invocation windows keyed by "${agentName}:${invId}" */
55
55
  windows: Record<string, InvocationWindow>;
56
+ /** Last tool-call threshold at which a compaction hint was issued */
57
+ lastCompactionHint: number;
56
58
  }
57
59
  /**
58
60
  * Represents a single agent invocation window with isolated guardrail budgets.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "6.1.1",
3
+ "version": "6.2.0",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",