opencode-swarm-plugin 0.37.0 → 0.39.1

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.
Files changed (79) hide show
  1. package/.env +2 -0
  2. package/.hive/eval-results.json +26 -0
  3. package/.hive/issues.jsonl +20 -5
  4. package/.hive/memories.jsonl +35 -1
  5. package/.opencode/eval-history.jsonl +12 -0
  6. package/.turbo/turbo-build.log +4 -4
  7. package/.turbo/turbo-test.log +319 -319
  8. package/CHANGELOG.md +258 -0
  9. package/README.md +50 -0
  10. package/bin/swarm.test.ts +475 -0
  11. package/bin/swarm.ts +385 -208
  12. package/dist/compaction-hook.d.ts +1 -1
  13. package/dist/compaction-hook.d.ts.map +1 -1
  14. package/dist/compaction-prompt-scoring.d.ts +124 -0
  15. package/dist/compaction-prompt-scoring.d.ts.map +1 -0
  16. package/dist/eval-capture.d.ts +81 -1
  17. package/dist/eval-capture.d.ts.map +1 -1
  18. package/dist/eval-gates.d.ts +84 -0
  19. package/dist/eval-gates.d.ts.map +1 -0
  20. package/dist/eval-history.d.ts +117 -0
  21. package/dist/eval-history.d.ts.map +1 -0
  22. package/dist/eval-learning.d.ts +216 -0
  23. package/dist/eval-learning.d.ts.map +1 -0
  24. package/dist/hive.d.ts +59 -0
  25. package/dist/hive.d.ts.map +1 -1
  26. package/dist/index.d.ts +87 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +823 -131
  29. package/dist/plugin.js +655 -131
  30. package/dist/post-compaction-tracker.d.ts +133 -0
  31. package/dist/post-compaction-tracker.d.ts.map +1 -0
  32. package/dist/swarm-decompose.d.ts +30 -0
  33. package/dist/swarm-decompose.d.ts.map +1 -1
  34. package/dist/swarm-orchestrate.d.ts +23 -0
  35. package/dist/swarm-orchestrate.d.ts.map +1 -1
  36. package/dist/swarm-prompts.d.ts +25 -1
  37. package/dist/swarm-prompts.d.ts.map +1 -1
  38. package/dist/swarm.d.ts +19 -0
  39. package/dist/swarm.d.ts.map +1 -1
  40. package/evals/README.md +595 -94
  41. package/evals/compaction-prompt.eval.ts +149 -0
  42. package/evals/coordinator-behavior.eval.ts +8 -8
  43. package/evals/fixtures/compaction-prompt-cases.ts +305 -0
  44. package/evals/lib/compaction-loader.test.ts +248 -0
  45. package/evals/lib/compaction-loader.ts +320 -0
  46. package/evals/lib/data-loader.test.ts +345 -0
  47. package/evals/lib/data-loader.ts +107 -6
  48. package/evals/scorers/compaction-prompt-scorers.ts +145 -0
  49. package/evals/scorers/compaction-scorers.ts +13 -13
  50. package/evals/scorers/coordinator-discipline.evalite-test.ts +3 -2
  51. package/evals/scorers/coordinator-discipline.ts +13 -13
  52. package/examples/plugin-wrapper-template.ts +177 -8
  53. package/package.json +7 -2
  54. package/scripts/migrate-unknown-sessions.ts +349 -0
  55. package/src/compaction-capture.integration.test.ts +257 -0
  56. package/src/compaction-hook.test.ts +139 -2
  57. package/src/compaction-hook.ts +113 -2
  58. package/src/compaction-prompt-scorers.test.ts +299 -0
  59. package/src/compaction-prompt-scoring.ts +298 -0
  60. package/src/eval-capture.test.ts +422 -0
  61. package/src/eval-capture.ts +94 -2
  62. package/src/eval-gates.test.ts +306 -0
  63. package/src/eval-gates.ts +218 -0
  64. package/src/eval-history.test.ts +508 -0
  65. package/src/eval-history.ts +214 -0
  66. package/src/eval-learning.test.ts +378 -0
  67. package/src/eval-learning.ts +360 -0
  68. package/src/index.ts +61 -1
  69. package/src/post-compaction-tracker.test.ts +251 -0
  70. package/src/post-compaction-tracker.ts +237 -0
  71. package/src/swarm-decompose.test.ts +40 -47
  72. package/src/swarm-decompose.ts +2 -2
  73. package/src/swarm-orchestrate.test.ts +270 -7
  74. package/src/swarm-orchestrate.ts +100 -13
  75. package/src/swarm-prompts.test.ts +121 -0
  76. package/src/swarm-prompts.ts +297 -4
  77. package/src/swarm-research.integration.test.ts +157 -0
  78. package/src/swarm-review.ts +3 -3
  79. /package/evals/{evalite.config.ts → evalite.config.ts.bak} +0 -0
@@ -64,7 +64,7 @@ describe("Compaction Hook", () => {
64
64
  describe("SWARM_COMPACTION_CONTEXT", () => {
65
65
  it("contains coordinator instructions", () => {
66
66
  expect(SWARM_COMPACTION_CONTEXT).toContain("COORDINATOR");
67
- expect(SWARM_COMPACTION_CONTEXT).toContain("You Are The COORDINATOR");
67
+ expect(SWARM_COMPACTION_CONTEXT).toContain("YOU ARE THE COORDINATOR");
68
68
  });
69
69
 
70
70
  it("contains prohibition-first anti-patterns", () => {
@@ -85,6 +85,48 @@ describe("Compaction Hook", () => {
85
85
  expect(SWARM_COMPACTION_CONTEXT).toContain("Blocked:");
86
86
  expect(SWARM_COMPACTION_CONTEXT).toContain("Completed:");
87
87
  });
88
+
89
+ // NEW: Full coordinator workflow must be present post-compaction
90
+ it("contains FULL coordinator workflow phases", () => {
91
+ // Phase 1.5: Research Phase
92
+ expect(SWARM_COMPACTION_CONTEXT).toContain("swarm_spawn_researcher");
93
+
94
+ // Phase 3: Decompose
95
+ expect(SWARM_COMPACTION_CONTEXT).toContain("swarm_select_strategy");
96
+ expect(SWARM_COMPACTION_CONTEXT).toContain("swarm_plan_prompt");
97
+ expect(SWARM_COMPACTION_CONTEXT).toContain("swarm_validate_decomposition");
98
+
99
+ // Phase 4: Create Cells
100
+ expect(SWARM_COMPACTION_CONTEXT).toContain("hive_create_epic");
101
+
102
+ // Phase 6: Spawn Workers
103
+ expect(SWARM_COMPACTION_CONTEXT).toContain("swarm_spawn_subtask");
104
+
105
+ // Phase 7: Review Loop
106
+ expect(SWARM_COMPACTION_CONTEXT).toContain("swarm_review");
107
+ expect(SWARM_COMPACTION_CONTEXT).toContain("swarm_review_feedback");
108
+ expect(SWARM_COMPACTION_CONTEXT).toContain("swarm_spawn_retry");
109
+ });
110
+
111
+ it("contains forbidden tools section with ALL forbidden tools", () => {
112
+ // Repository fetching
113
+ expect(SWARM_COMPACTION_CONTEXT).toContain("repo-crawl_file");
114
+ expect(SWARM_COMPACTION_CONTEXT).toContain("repo-autopsy");
115
+
116
+ // Web/documentation fetching
117
+ expect(SWARM_COMPACTION_CONTEXT).toContain("webfetch");
118
+ expect(SWARM_COMPACTION_CONTEXT).toContain("fetch_fetch");
119
+ expect(SWARM_COMPACTION_CONTEXT).toContain("context7");
120
+
121
+ // Knowledge base
122
+ expect(SWARM_COMPACTION_CONTEXT).toContain("pdf-brain");
123
+ });
124
+
125
+ it("contains strategy reference table", () => {
126
+ expect(SWARM_COMPACTION_CONTEXT).toContain("file-based");
127
+ expect(SWARM_COMPACTION_CONTEXT).toContain("feature-based");
128
+ expect(SWARM_COMPACTION_CONTEXT).toContain("risk-based");
129
+ });
88
130
  });
89
131
 
90
132
  describe("SWARM_DETECTION_FALLBACK", () => {
@@ -136,7 +178,7 @@ describe("Compaction Hook", () => {
136
178
  it("HIGH confidence triggers full context", async () => {
137
179
  // This would need proper mocking of active reservations
138
180
  // For now, just verify the context strings exist
139
- expect(SWARM_COMPACTION_CONTEXT).toContain("SWARM ACTIVE");
181
+ expect(SWARM_COMPACTION_CONTEXT).toContain("YOU ARE THE COORDINATOR");
140
182
  });
141
183
 
142
184
  it("LOW confidence triggers fallback prompt", async () => {
@@ -145,6 +187,101 @@ describe("Compaction Hook", () => {
145
187
  });
146
188
  });
147
189
 
190
+ describe("Forbidden tools anti-pattern (TDD red phase)", () => {
191
+ it("SWARM_COMPACTION_CONTEXT includes 'NEVER fetch directly' rule", () => {
192
+ // Should warn against direct fetching
193
+ expect(SWARM_COMPACTION_CONTEXT).toContain("NEVER");
194
+ expect(SWARM_COMPACTION_CONTEXT).toContain("repo-crawl");
195
+ expect(SWARM_COMPACTION_CONTEXT).toContain("webfetch");
196
+ expect(SWARM_COMPACTION_CONTEXT).toContain("fetch_fetch");
197
+ expect(SWARM_COMPACTION_CONTEXT).toContain("context7");
198
+ expect(SWARM_COMPACTION_CONTEXT).toContain("pdf-brain");
199
+ });
200
+
201
+ it("SWARM_COMPACTION_CONTEXT instructs to spawn researcher instead", () => {
202
+ expect(SWARM_COMPACTION_CONTEXT).toContain("SPAWN A RESEARCHER");
203
+ expect(SWARM_COMPACTION_CONTEXT).toContain("swarm_spawn_researcher");
204
+ });
205
+
206
+ it("lists all forbidden repo-crawl tools", () => {
207
+ const forbiddenTools = [
208
+ "repo-crawl_file",
209
+ "repo-crawl_readme",
210
+ "repo-crawl_search",
211
+ "repo-crawl_structure",
212
+ "repo-crawl_tree"
213
+ ];
214
+
215
+ for (const tool of forbiddenTools) {
216
+ expect(SWARM_COMPACTION_CONTEXT).toContain(tool);
217
+ }
218
+ });
219
+
220
+ it("lists all forbidden repo-autopsy tools", () => {
221
+ expect(SWARM_COMPACTION_CONTEXT).toContain("repo-autopsy");
222
+ });
223
+
224
+ it("lists all forbidden context7 tools", () => {
225
+ const forbiddenTools = [
226
+ "context7_resolve-library-id",
227
+ "context7_get-library-docs"
228
+ ];
229
+
230
+ for (const tool of forbiddenTools) {
231
+ expect(SWARM_COMPACTION_CONTEXT).toContain(tool);
232
+ }
233
+ });
234
+
235
+ it("lists all forbidden pdf-brain tools", () => {
236
+ const forbiddenTools = [
237
+ "pdf-brain_search",
238
+ "pdf-brain_read"
239
+ ];
240
+
241
+ for (const tool of forbiddenTools) {
242
+ expect(SWARM_COMPACTION_CONTEXT).toContain(tool);
243
+ }
244
+ });
245
+ });
246
+
247
+ describe("Coordinator identity reinforcement (TDD red phase)", () => {
248
+ it("includes ASCII header for coordinator identity", () => {
249
+ // Should have prominent visual indicator
250
+ expect(SWARM_COMPACTION_CONTEXT).toMatch(/[╔═╗║╚╝]|[┌─┐│└┘]|[█▀▄]/);
251
+ });
252
+
253
+ it("repeats 'YOU ARE THE COORDINATOR' multiple times", () => {
254
+ const matches = SWARM_COMPACTION_CONTEXT.match(/YOU ARE THE COORDINATOR/gi);
255
+ expect(matches).toBeDefined();
256
+ expect(matches!.length).toBeGreaterThanOrEqual(2);
257
+ });
258
+
259
+ it("uses strong imperative language NEVER/ALWAYS/NON-NEGOTIABLE", () => {
260
+ expect(SWARM_COMPACTION_CONTEXT).toContain("NEVER");
261
+ expect(SWARM_COMPACTION_CONTEXT).toContain("ALWAYS");
262
+ expect(SWARM_COMPACTION_CONTEXT).toContain("NON-NEGOTIABLE");
263
+ });
264
+
265
+ it("makes role unmistakable with multiple strong statements", () => {
266
+ // Check for strong coordinator identity statements
267
+ const identityPatterns = [
268
+ /YOU ARE THE COORDINATOR/i,
269
+ /NOT A WORKER/i,
270
+ /ORCHESTRATE/i,
271
+ /DO NOT IMPLEMENT/i
272
+ ];
273
+
274
+ let matchCount = 0;
275
+ for (const pattern of identityPatterns) {
276
+ if (pattern.test(SWARM_COMPACTION_CONTEXT)) {
277
+ matchCount++;
278
+ }
279
+ }
280
+
281
+ expect(matchCount).toBeGreaterThanOrEqual(3);
282
+ });
283
+ });
284
+
148
285
  describe("Specific swarm state injection (TDD red phase)", () => {
149
286
  it("includes specific epic ID when in_progress epic exists", async () => {
150
287
  // Mock hive with an in_progress epic
@@ -68,9 +68,21 @@ function getLog() {
68
68
  * This is NOT about preserving state for a human - it's about the swarm continuing
69
69
  * autonomously after context compression.
70
70
  */
71
- export const SWARM_COMPACTION_CONTEXT = `## 🐝 SWARM ACTIVE - You Are The COORDINATOR
71
+ export const SWARM_COMPACTION_CONTEXT = `
72
+ ┌─────────────────────────────────────────────────────────────┐
73
+ │ │
74
+ │ 🐝 YOU ARE THE COORDINATOR 🐝 │
75
+ │ │
76
+ │ NOT A WORKER. NOT AN IMPLEMENTER. │
77
+ │ YOU ORCHESTRATE. │
78
+ │ │
79
+ └─────────────────────────────────────────────────────────────┘
72
80
 
73
- Context was compacted but the swarm is still running. You are the **COORDINATOR**.
81
+ ## 🎯 NON-NEGOTIABLE: YOU ARE THE COORDINATOR
82
+
83
+ Context was compacted but the swarm is still running. **YOU ARE THE COORDINATOR.**
84
+
85
+ Your role is ORCHESTRATION, not implementation. When you catch yourself about to do work directly, STOP.
74
86
 
75
87
  ### ⛔ NEVER DO THESE (Coordinator Anti-Patterns)
76
88
 
@@ -81,9 +93,27 @@ Context was compacted but the swarm is still running. You are the **COORDINATOR*
81
93
  - ❌ **NEVER** implement features yourself - SPAWN A WORKER
82
94
  - ❌ **NEVER** "just do it myself to save time" - NO. SPAWN A WORKER.
83
95
  - ❌ **NEVER** reserve files with \`swarmmail_reserve\` - Workers reserve files
96
+ - ❌ **NEVER** fetch files/docs directly - SPAWN A RESEARCHER
84
97
 
85
98
  **If you catch yourself about to edit a file, STOP. Use \`swarm_spawn_subtask\` instead.**
86
99
 
100
+ ### 🚫 FORBIDDEN TOOLS (Coordinators MUST delegate these)
101
+
102
+ **NEVER use these tools directly. ALWAYS spawn a researcher worker via \`swarm_spawn_researcher\`:**
103
+
104
+ **Repository fetching:**
105
+ - \`repo-crawl_file\`, \`repo-crawl_readme\`, \`repo-crawl_search\`, \`repo-crawl_structure\`, \`repo-crawl_tree\`
106
+ - \`repo-autopsy_*\` (all repo-autopsy tools)
107
+
108
+ **Web/documentation fetching:**
109
+ - \`webfetch\`, \`fetch_fetch\`
110
+ - \`context7_resolve-library-id\`, \`context7_get-library-docs\`
111
+
112
+ **Knowledge base:**
113
+ - \`pdf-brain_search\`, \`pdf-brain_read\`
114
+
115
+ **If you need external data:** Use \`swarm_spawn_researcher\` with a clear research task. The researcher will fetch, summarize, and return findings.
116
+
87
117
  ### ✅ ALWAYS DO THESE (Coordinator Checklist)
88
118
 
89
119
  On resume, execute this checklist IN ORDER:
@@ -133,6 +163,87 @@ Extract from session context:
133
163
  - **Review work** - Use \`swarm_review\` and \`swarm_review_feedback\` for completed work
134
164
  - **Close the loop** - When all subtasks done, verify and close the epic
135
165
 
166
+ **You are the COORDINATOR. You orchestrate. You do NOT implement. Spawn workers.**
167
+
168
+ ---
169
+
170
+ ## 📋 FULL COORDINATOR WORKFLOW (Reference)
171
+
172
+ You are ALWAYS swarming. Here is the complete workflow for any new work:
173
+
174
+ ### Phase 1.5: Research Phase (FOR COMPLEX TASKS)
175
+
176
+ **If the task requires understanding unfamiliar technologies, spawn a researcher FIRST:**
177
+
178
+ \`\`\`
179
+ swarm_spawn_researcher(
180
+ research_id="research-<topic>",
181
+ epic_id="<epic-id>",
182
+ tech_stack=["<technology>"],
183
+ project_path="<path>"
184
+ )
185
+ // Then spawn with Task(subagent_type="swarm/researcher", prompt="<from above>")
186
+ \`\`\`
187
+
188
+ ### Phase 2: Knowledge Gathering
189
+
190
+ \`\`\`
191
+ semantic-memory_find(query="<task keywords>", limit=5) # Past learnings
192
+ cass_search(query="<task description>", limit=5) # Similar past tasks
193
+ skills_list() # Available skills
194
+ \`\`\`
195
+
196
+ ### Phase 3: Decompose
197
+
198
+ \`\`\`
199
+ swarm_select_strategy(task="<task>")
200
+ swarm_plan_prompt(task="<task>", context="<synthesized knowledge>")
201
+ swarm_validate_decomposition(response="<CellTree JSON>")
202
+ \`\`\`
203
+
204
+ ### Phase 4: Create Cells
205
+
206
+ \`hive_create_epic(epic_title="<task>", subtasks=[...])\`
207
+
208
+ ### Phase 5: DO NOT Reserve Files
209
+
210
+ > **⚠️ Coordinator NEVER reserves files.** Workers reserve their own files.
211
+
212
+ ### Phase 6: Spawn Workers
213
+
214
+ \`\`\`
215
+ swarm_spawn_subtask(bead_id, epic_id, title, files, shared_context, project_path)
216
+ Task(subagent_type="swarm/worker", prompt="<from above>")
217
+ \`\`\`
218
+
219
+ ### Phase 7: MANDATORY Review Loop
220
+
221
+ **AFTER EVERY Task() RETURNS:**
222
+
223
+ 1. \`swarmmail_inbox()\` - Check for messages
224
+ 2. \`swarm_review(project_key, epic_id, task_id, files_touched)\` - Generate review
225
+ 3. Evaluate against epic goals
226
+ 4. \`swarm_review_feedback(project_key, task_id, worker_id, status, issues)\`
227
+
228
+ **If needs_changes:**
229
+ \`\`\`
230
+ swarm_spawn_retry(bead_id, epic_id, original_prompt, attempt, issues, diff, files, project_path)
231
+ // Spawn NEW worker with Task() using retry prompt
232
+ // Max 3 attempts before marking task blocked
233
+ \`\`\`
234
+
235
+ ### Phase 8: Complete
236
+
237
+ \`hive_sync()\` - Sync all cells to git
238
+
239
+ ## Strategy Reference
240
+
241
+ | Strategy | Best For | Keywords |
242
+ | -------------- | ------------------------ | -------------------------------------- |
243
+ | file-based | Refactoring, migrations | refactor, migrate, rename, update all |
244
+ | feature-based | New features | add, implement, build, create, feature |
245
+ | risk-based | Bug fixes, security | fix, bug, security, critical, urgent |
246
+
136
247
  **You are the COORDINATOR. You orchestrate. You do NOT implement. Spawn workers.**
137
248
  `;
138
249
 
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Tests for compaction prompt quality scorers
3
+ *
4
+ * TDD approach - tests written FIRST to define scorer behavior
5
+ * Tests the PURE scoring functions (not evalite wrappers)
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import type { CompactionPrompt } from "./compaction-prompt-scoring.js";
10
+ import {
11
+ scoreActionability,
12
+ scoreCoordinatorIdentity,
13
+ scoreEpicIdSpecificity,
14
+ scoreForbiddenToolsPresent,
15
+ scorePostCompactionDiscipline,
16
+ } from "./compaction-prompt-scoring.js";
17
+
18
+ describe("epicIdSpecificity scorer", () => {
19
+ test("scores 1.0 for real epic IDs", () => {
20
+ const prompt: CompactionPrompt = {
21
+ content: "Continue coordinating epic mjkw81rkq4c",
22
+ };
23
+
24
+ const result = scoreEpicIdSpecificity(prompt);
25
+
26
+ expect(result.score).toBe(1.0);
27
+ expect(result.message).toContain("real epic ID");
28
+ });
29
+
30
+ test("scores 0.0 for placeholder IDs like <epic-id>", () => {
31
+ const prompt: CompactionPrompt = {
32
+ content: "Continue coordinating epic <epic-id>",
33
+ };
34
+
35
+ const result = scoreEpicIdSpecificity(prompt);
36
+
37
+ expect(result.score).toBe(0.0);
38
+ expect(result.message).toContain("placeholder");
39
+ });
40
+
41
+ test("scores 0.0 for bd-xxx placeholders", () => {
42
+ const prompt: CompactionPrompt = {
43
+ content: "Check status of bd-xxx",
44
+ };
45
+
46
+ const result = scoreEpicIdSpecificity(prompt);
47
+
48
+ expect(result.score).toBe(0.0);
49
+ expect(result.message).toContain("placeholder");
50
+ });
51
+
52
+ test("scores 0.0 for generic <path> placeholders", () => {
53
+ const prompt: CompactionPrompt = {
54
+ content: "Project at <path>",
55
+ };
56
+
57
+ const result = scoreEpicIdSpecificity(prompt);
58
+
59
+ expect(result.score).toBe(0.0);
60
+ });
61
+
62
+ test("scores 0.0 when no epic ID found", () => {
63
+ const prompt: CompactionPrompt = {
64
+ content: "Continue working on the task",
65
+ };
66
+
67
+ const result = scoreEpicIdSpecificity(prompt);
68
+
69
+ expect(result.score).toBe(0.0);
70
+ expect(result.message).toContain("No epic ID");
71
+ });
72
+ });
73
+
74
+ describe("actionability scorer", () => {
75
+ test("scores 1.0 when swarm_status has real epic ID", () => {
76
+ const prompt: CompactionPrompt = {
77
+ content: `First action:
78
+ swarm_status(epic_id='mjkw81rkq4c', project_key='/path/to/project')`,
79
+ };
80
+
81
+ const result = scoreActionability(prompt);
82
+
83
+ expect(result.score).toBe(1.0);
84
+ expect(result.message).toContain("actionable tool call");
85
+ });
86
+
87
+ test("scores 1.0 when swarmmail_inbox is present", () => {
88
+ const prompt: CompactionPrompt = {
89
+ content: `Check messages:
90
+ swarmmail_inbox()`,
91
+ };
92
+
93
+ const result = scoreActionability(prompt);
94
+
95
+ expect(result.score).toBe(1.0);
96
+ expect(result.message).toContain("actionable tool call");
97
+ });
98
+
99
+ test("scores 0.0 for generic instructions without tool calls", () => {
100
+ const prompt: CompactionPrompt = {
101
+ content: "Check the status of workers and review progress",
102
+ };
103
+
104
+ const result = scoreActionability(prompt);
105
+
106
+ expect(result.score).toBe(0.0);
107
+ expect(result.message).toContain("No actionable");
108
+ });
109
+
110
+ test("scores 0.0 for swarm_status with placeholders", () => {
111
+ const prompt: CompactionPrompt = {
112
+ content: `swarm_status(epic_id='<epic-id>', project_key='<path>')`,
113
+ };
114
+
115
+ const result = scoreActionability(prompt);
116
+
117
+ expect(result.score).toBe(0.0);
118
+ expect(result.message).toContain("placeholder");
119
+ });
120
+ });
121
+
122
+ describe("coordinatorIdentity scorer", () => {
123
+ test("scores 1.0 with ASCII header and strong mandates", () => {
124
+ const prompt: CompactionPrompt = {
125
+ content: `┌─────────────────────────────────────────┐
126
+ │ YOU ARE THE COORDINATOR │
127
+ │ │
128
+ │ NEVER spawn workers yourself │
129
+ │ ALWAYS review worker output │
130
+ └─────────────────────────────────────────┘
131
+
132
+ Continue coordinating the swarm.`,
133
+ };
134
+
135
+ const result = scoreCoordinatorIdentity(prompt);
136
+
137
+ expect(result.score).toBe(1.0);
138
+ expect(result.message).toContain("ASCII header");
139
+ expect(result.message).toContain("strong mandates");
140
+ });
141
+
142
+ test("scores 0.5 with ASCII header but weak language", () => {
143
+ const prompt: CompactionPrompt = {
144
+ content: `┌─────────────────────────────────────────┐
145
+ │ COORDINATOR MODE │
146
+ └─────────────────────────────────────────┘
147
+
148
+ You should consider delegating work.`,
149
+ };
150
+
151
+ const result = scoreCoordinatorIdentity(prompt);
152
+
153
+ expect(result.score).toBe(0.5);
154
+ expect(result.message).toContain("weak language");
155
+ });
156
+
157
+ test("scores 0.0 without ASCII header", () => {
158
+ const prompt: CompactionPrompt = {
159
+ content: `You are the coordinator. NEVER do work directly. ALWAYS delegate.`,
160
+ };
161
+
162
+ const result = scoreCoordinatorIdentity(prompt);
163
+
164
+ expect(result.score).toBe(0.0);
165
+ expect(result.message).toContain("No ASCII header");
166
+ });
167
+ });
168
+
169
+ describe("forbiddenToolsPresent scorer", () => {
170
+ test("scores 1.0 when all forbidden tools listed", () => {
171
+ const prompt: CompactionPrompt = {
172
+ content: `🚫 FORBIDDEN TOOLS - NEVER call these:
173
+ - Edit (use swarm_spawn_subtask)
174
+ - Write (use swarm_spawn_subtask)
175
+ - swarmmail_reserve (only workers reserve)
176
+ - bash with git commit (workers commit)`,
177
+ };
178
+
179
+ const result = scoreForbiddenToolsPresent(prompt);
180
+
181
+ expect(result.score).toBe(1.0);
182
+ expect(result.message).toContain("All 4 forbidden tools");
183
+ });
184
+
185
+ test("scores 0.75 when 3 out of 4 tools listed", () => {
186
+ const prompt: CompactionPrompt = {
187
+ content: `🚫 FORBIDDEN TOOLS:
188
+ - Edit
189
+ - Write
190
+ - swarmmail_reserve`,
191
+ };
192
+
193
+ const result = scoreForbiddenToolsPresent(prompt);
194
+
195
+ expect(result.score).toBe(0.75);
196
+ expect(result.message).toContain("3/4");
197
+ });
198
+
199
+ test("scores 0.5 when 2 out of 4 tools listed", () => {
200
+ const prompt: CompactionPrompt = {
201
+ content: `Don't use Edit or Write directly.`,
202
+ };
203
+
204
+ const result = scoreForbiddenToolsPresent(prompt);
205
+
206
+ expect(result.score).toBe(0.5);
207
+ expect(result.message).toContain("2/4");
208
+ });
209
+
210
+ test("scores 0.0 when no forbidden tools listed", () => {
211
+ const prompt: CompactionPrompt = {
212
+ content: "Continue coordinating the epic",
213
+ };
214
+
215
+ const result = scoreForbiddenToolsPresent(prompt);
216
+
217
+ expect(result.score).toBe(0.0);
218
+ expect(result.message).toContain("0/4");
219
+ });
220
+ });
221
+
222
+ describe("postCompactionDiscipline scorer", () => {
223
+ test("scores 1.0 when first tool is swarm_status", () => {
224
+ const prompt: CompactionPrompt = {
225
+ content: `Resume coordination:
226
+
227
+ 1. swarm_status(epic_id='mjkw81rkq4c')
228
+ 2. Check inbox
229
+ 3. Review progress`,
230
+ };
231
+
232
+ const result = scorePostCompactionDiscipline(prompt);
233
+
234
+ expect(result.score).toBe(1.0);
235
+ expect(result.message).toContain("swarm_status");
236
+ expect(result.message).toContain("correct");
237
+ });
238
+
239
+ test("scores 1.0 when first tool is swarmmail_inbox", () => {
240
+ const prompt: CompactionPrompt = {
241
+ content: `Next steps:
242
+ 1. swarmmail_inbox()
243
+ 2. Review messages`,
244
+ };
245
+
246
+ const result = scorePostCompactionDiscipline(prompt);
247
+
248
+ expect(result.score).toBe(1.0);
249
+ expect(result.message).toContain("inbox");
250
+ expect(result.message).toContain("correct");
251
+ });
252
+
253
+ test("scores 0.0 when first tool is Edit", () => {
254
+ const prompt: CompactionPrompt = {
255
+ content: `Resume:
256
+ 1. Edit(file='src/auth.ts', ...)
257
+ 2. Check status`,
258
+ };
259
+
260
+ const result = scorePostCompactionDiscipline(prompt);
261
+
262
+ expect(result.score).toBe(0.0);
263
+ expect(result.message).toContain("Edit");
264
+ });
265
+
266
+ test("scores 0.0 when first tool is Write", () => {
267
+ const prompt: CompactionPrompt = {
268
+ content: `1. Write(file='README.md', ...)`,
269
+ };
270
+
271
+ const result = scorePostCompactionDiscipline(prompt);
272
+
273
+ expect(result.score).toBe(0.0);
274
+ expect(result.message).toContain("Write");
275
+ });
276
+
277
+ test("scores 0.0 when first tool is Read", () => {
278
+ const prompt: CompactionPrompt = {
279
+ content: `1. Read(file='src/index.ts')
280
+ 2. swarm_status()`,
281
+ };
282
+
283
+ const result = scorePostCompactionDiscipline(prompt);
284
+
285
+ expect(result.score).toBe(0.0);
286
+ expect(result.message).toContain("Read");
287
+ });
288
+
289
+ test("scores 0.0 when no tool calls mentioned", () => {
290
+ const prompt: CompactionPrompt = {
291
+ content: "Continue coordinating the epic",
292
+ };
293
+
294
+ const result = scorePostCompactionDiscipline(prompt);
295
+
296
+ expect(result.score).toBe(0.0);
297
+ expect(result.message).toContain("No tool");
298
+ });
299
+ });