opencode-swarm-plugin 0.37.0 → 0.38.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.
@@ -6,10 +6,13 @@
6
6
  * - Researcher spawning for identified technologies
7
7
  * - Summary collection from semantic-memory
8
8
  * - Research result aggregation
9
+ * - Eval capture integration (captureSubtaskOutcome wiring)
9
10
  */
10
11
 
11
- import { describe, test, expect, beforeEach } from "bun:test";
12
- import { runResearchPhase, extractTechStack } from "./swarm-orchestrate";
12
+ import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
13
+ import { runResearchPhase, extractTechStack, swarm_complete } from "./swarm-orchestrate";
14
+ import * as evalCapture from "./eval-capture.js";
15
+ import * as fs from "node:fs";
13
16
 
14
17
  describe("extractTechStack", () => {
15
18
  test("extracts Next.js from task description", () => {
@@ -115,9 +118,269 @@ describe("runResearchPhase", () => {
115
118
  });
116
119
  });
117
120
 
118
- describe("swarm_research_phase tool", () => {
119
- test.todo("exposes research phase as plugin tool");
120
- test.todo("validates task parameter");
121
- test.todo("validates project_path parameter");
122
- test.todo("returns JSON string with research results");
121
+ // describe("swarm_research_phase tool", () => {
122
+ // test.todo("exposes research phase as plugin tool");
123
+ // test.todo("validates task parameter");
124
+ // test.todo("validates project_path parameter");
125
+ // test.todo("returns JSON string with research results");
126
+ // });
127
+
128
+ // ============================================================================
129
+ // Eval Capture Integration Tests (swarm_complete)
130
+ // ============================================================================
131
+
132
+ describe("captureSubtaskOutcome integration", () => {
133
+ const mockContext = {
134
+ sessionID: `test-complete-${Date.now()}`,
135
+ messageID: `test-message-${Date.now()}`,
136
+ agent: "test-agent",
137
+ abort: new AbortController().signal,
138
+ };
139
+
140
+ let testProjectPath: string;
141
+
142
+ beforeEach(async () => {
143
+ testProjectPath = `/tmp/test-swarm-complete-${Date.now()}`;
144
+ fs.mkdirSync(testProjectPath, { recursive: true });
145
+
146
+ // Create .hive directory and issues.jsonl
147
+ const hiveDir = `${testProjectPath}/.hive`;
148
+ fs.mkdirSync(hiveDir, { recursive: true });
149
+ fs.writeFileSync(`${hiveDir}/issues.jsonl`, "", "utf-8");
150
+
151
+ // Set hive working directory to testProjectPath
152
+ const { setHiveWorkingDirectory } = await import("./hive");
153
+ setHiveWorkingDirectory(testProjectPath);
154
+ });
155
+
156
+ afterEach(() => {
157
+ if (fs.existsSync(testProjectPath)) {
158
+ fs.rmSync(testProjectPath, { recursive: true, force: true });
159
+ }
160
+ });
161
+
162
+ test("calls captureSubtaskOutcome after successful completion with all params", async () => {
163
+ // Import hive tools
164
+ const { hive_create_epic } = await import("./hive");
165
+
166
+ // Spy on captureSubtaskOutcome
167
+ const captureOutcomeSpy = spyOn(evalCapture, "captureSubtaskOutcome");
168
+
169
+ // Create an epic with a subtask using hive_create_epic
170
+ const epicResult = await hive_create_epic.execute({
171
+ epic_title: "Add OAuth",
172
+ epic_description: "Implement OAuth authentication",
173
+ subtasks: [
174
+ {
175
+ title: "Add auth service",
176
+ priority: 2,
177
+ files: ["src/auth/service.ts", "src/auth/schema.ts"],
178
+ },
179
+ ],
180
+ }, mockContext);
181
+
182
+ const epicData = JSON.parse(epicResult);
183
+ expect(epicData.success).toBe(true);
184
+
185
+ const epicId = epicData.epic.id;
186
+ const beadId = epicData.subtasks[0].id;
187
+
188
+ const startTime = Date.now() - 120000; // Started 2 minutes ago
189
+ const plannedFiles = ["src/auth/service.ts", "src/auth/schema.ts"];
190
+ const actualFiles = ["src/auth/service.ts", "src/auth/schema.ts", "src/auth/types.ts"];
191
+
192
+ // Call swarm_complete
193
+ const result = await swarm_complete.execute(
194
+ {
195
+ project_key: testProjectPath,
196
+ agent_name: "TestAgent",
197
+ bead_id: beadId,
198
+ summary: "Implemented OAuth service with JWT strategy",
199
+ files_touched: actualFiles,
200
+ skip_verification: true, // Skip verification for test
201
+ skip_review: true, // Skip review for test
202
+ planned_files: plannedFiles,
203
+ start_time: startTime,
204
+ error_count: 0,
205
+ retry_count: 0,
206
+ },
207
+ mockContext,
208
+ );
209
+
210
+ const parsed = JSON.parse(result);
211
+ expect(parsed.success).toBe(true);
212
+
213
+ // Verify captureSubtaskOutcome was called with correct params
214
+ expect(captureOutcomeSpy).toHaveBeenCalledTimes(1);
215
+
216
+ const call = captureOutcomeSpy.mock.calls[0][0];
217
+ expect(call.epicId).toBe(epicId);
218
+ expect(call.projectPath).toBe(testProjectPath);
219
+ expect(call.beadId).toBe(beadId);
220
+ expect(call.title).toBe("Add auth service");
221
+ expect(call.plannedFiles).toEqual(plannedFiles);
222
+ expect(call.actualFiles).toEqual(actualFiles);
223
+ expect(call.durationMs).toBeGreaterThan(0);
224
+ expect(call.errorCount).toBe(0);
225
+ expect(call.retryCount).toBe(0);
226
+ expect(call.success).toBe(true);
227
+
228
+ captureOutcomeSpy.mockRestore();
229
+ });
230
+
231
+ test("does not call captureSubtaskOutcome when required params missing", async () => {
232
+ const { hive_create_epic } = await import("./hive");
233
+ const captureOutcomeSpy = spyOn(evalCapture, "captureSubtaskOutcome");
234
+
235
+ // Create an epic with a subtask
236
+ const epicResult = await hive_create_epic.execute({
237
+ epic_title: "Fix bug",
238
+ subtasks: [
239
+ {
240
+ title: "Fix auth bug",
241
+ priority: 1,
242
+ files: ["src/auth.ts"],
243
+ },
244
+ ],
245
+ }, mockContext);
246
+
247
+ const epicData = JSON.parse(epicResult);
248
+ const beadId = epicData.subtasks[0].id;
249
+
250
+ // Call without planned_files or start_time
251
+ const result = await swarm_complete.execute(
252
+ {
253
+ project_key: testProjectPath,
254
+ agent_name: "TestAgent",
255
+ bead_id: beadId,
256
+ summary: "Fixed the bug",
257
+ skip_verification: true,
258
+ skip_review: true,
259
+ // No planned_files, start_time
260
+ },
261
+ mockContext,
262
+ );
263
+
264
+ const parsed = JSON.parse(result);
265
+ expect(parsed.success).toBe(true);
266
+
267
+ // Capture should still be called, but with default values
268
+ // (The function is called in all success cases, it just handles missing params)
269
+ expect(captureOutcomeSpy).toHaveBeenCalledTimes(1);
270
+
271
+ captureOutcomeSpy.mockRestore();
272
+ });
273
+ });
274
+
275
+ // ============================================================================
276
+ // Eval Capture Integration Tests (swarm_record_outcome)
277
+ // ============================================================================
278
+
279
+ describe("finalizeEvalRecord integration", () => {
280
+ const mockContext = {
281
+ sessionID: `test-finalize-${Date.now()}`,
282
+ messageID: `test-message-${Date.now()}`,
283
+ agent: "test-agent",
284
+ abort: new AbortController().signal,
285
+ };
286
+
287
+ test("calls finalizeEvalRecord when project_path and epic_id provided", async () => {
288
+ const { swarm_record_outcome } = await import("./swarm-orchestrate");
289
+
290
+ // Spy on finalizeEvalRecord
291
+ const finalizeEvalSpy = spyOn(evalCapture, "finalizeEvalRecord");
292
+ finalizeEvalSpy.mockReturnValue(null); // Mock return value
293
+
294
+ const testProjectPath = "/tmp/test-project";
295
+ const testEpicId = "bd-test123";
296
+ const testBeadId = `${testEpicId}.0`;
297
+
298
+ // Call swarm_record_outcome with epic_id and project_path
299
+ await swarm_record_outcome.execute({
300
+ bead_id: testBeadId,
301
+ duration_ms: 120000,
302
+ error_count: 0,
303
+ retry_count: 0,
304
+ success: true,
305
+ files_touched: ["src/test.ts"],
306
+ epic_id: testEpicId,
307
+ project_path: testProjectPath,
308
+ }, mockContext);
309
+
310
+ // Verify finalizeEvalRecord was called
311
+ expect(finalizeEvalSpy).toHaveBeenCalledTimes(1);
312
+ expect(finalizeEvalSpy).toHaveBeenCalledWith({
313
+ epicId: testEpicId,
314
+ projectPath: testProjectPath,
315
+ });
316
+
317
+ finalizeEvalSpy.mockRestore();
318
+ });
319
+
320
+ test("does not call finalizeEvalRecord when epic_id or project_path missing", async () => {
321
+ const { swarm_record_outcome } = await import("./swarm-orchestrate");
322
+
323
+ // Spy on finalizeEvalRecord
324
+ const finalizeEvalSpy = spyOn(evalCapture, "finalizeEvalRecord");
325
+
326
+ const testBeadId = "bd-test123.0";
327
+
328
+ // Call without epic_id or project_path
329
+ await swarm_record_outcome.execute({
330
+ bead_id: testBeadId,
331
+ duration_ms: 120000,
332
+ error_count: 0,
333
+ retry_count: 0,
334
+ success: true,
335
+ }, mockContext);
336
+
337
+ // Verify finalizeEvalRecord was NOT called
338
+ expect(finalizeEvalSpy).toHaveBeenCalledTimes(0);
339
+
340
+ finalizeEvalSpy.mockRestore();
341
+ });
342
+
343
+ test("includes finalized record in response when available", async () => {
344
+ const { swarm_record_outcome } = await import("./swarm-orchestrate");
345
+
346
+ // Mock finalizeEvalRecord to return a record
347
+ const mockFinalRecord = {
348
+ id: "bd-test123",
349
+ timestamp: new Date().toISOString(),
350
+ project_path: "/tmp/test-project",
351
+ task: "Test task",
352
+ strategy: "file-based" as const,
353
+ subtask_count: 2,
354
+ epic_title: "Test Epic",
355
+ subtasks: [],
356
+ overall_success: true,
357
+ total_duration_ms: 240000,
358
+ total_errors: 0,
359
+ };
360
+
361
+ const finalizeEvalSpy = spyOn(evalCapture, "finalizeEvalRecord");
362
+ finalizeEvalSpy.mockReturnValue(mockFinalRecord);
363
+
364
+ const testProjectPath = "/tmp/test-project";
365
+ const testEpicId = "bd-test123";
366
+ const testBeadId = `${testEpicId}.0`;
367
+
368
+ // Call with epic_id and project_path
369
+ const result = await swarm_record_outcome.execute({
370
+ bead_id: testBeadId,
371
+ duration_ms: 120000,
372
+ error_count: 0,
373
+ retry_count: 0,
374
+ success: true,
375
+ epic_id: testEpicId,
376
+ project_path: testProjectPath,
377
+ }, mockContext);
378
+
379
+ // Parse result and check for finalized record
380
+ const parsed = JSON.parse(result);
381
+ expect(parsed).toHaveProperty("finalized_eval_record");
382
+ expect(parsed.finalized_eval_record).toEqual(mockFinalRecord);
383
+
384
+ finalizeEvalSpy.mockRestore();
385
+ });
123
386
  });
@@ -83,7 +83,8 @@ import {
83
83
  isReviewApproved,
84
84
  getReviewStatus,
85
85
  } from "./swarm-review";
86
- import { captureCoordinatorEvent } from "./eval-capture.js";
86
+ import { captureCoordinatorEvent, type EvalRecord } from "./eval-capture.js";
87
+ import { formatResearcherPrompt } from "./swarm-prompts";
87
88
 
88
89
  // ============================================================================
89
90
  // Helper Functions
@@ -1710,6 +1711,31 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
1710
1711
  },
1711
1712
  };
1712
1713
 
1714
+ // Capture subtask completion outcome for eval data
1715
+ try {
1716
+ const { captureSubtaskOutcome } = await import("./eval-capture.js");
1717
+ const durationMs = args.start_time ? Date.now() - args.start_time : 0;
1718
+
1719
+ // Determine epic ID: use parent_id if available, otherwise fall back to extracting from bead_id
1720
+ const evalEpicId = cell.parent_id || epicId;
1721
+
1722
+ captureSubtaskOutcome({
1723
+ epicId: evalEpicId,
1724
+ projectPath: args.project_key,
1725
+ beadId: args.bead_id,
1726
+ title: cell.title,
1727
+ plannedFiles: args.planned_files || [],
1728
+ actualFiles: args.files_touched || [],
1729
+ durationMs,
1730
+ errorCount: args.error_count || 0,
1731
+ retryCount: args.retry_count || 0,
1732
+ success: true,
1733
+ });
1734
+ } catch (error) {
1735
+ // Non-fatal - don't block completion if capture fails
1736
+ console.warn("[swarm_complete] Failed to capture subtask outcome:", error);
1737
+ }
1738
+
1713
1739
  // Capture subtask completion outcome
1714
1740
  try {
1715
1741
  const durationMs = args.start_time ? Date.now() - args.start_time : 0;
@@ -1946,6 +1972,14 @@ export const swarm_record_outcome = tool({
1946
1972
  .string()
1947
1973
  .optional()
1948
1974
  .describe("Detailed failure context (error message, stack trace, etc.)"),
1975
+ project_path: tool.schema
1976
+ .string()
1977
+ .optional()
1978
+ .describe("Project path (for finalizing eval records when all subtasks complete)"),
1979
+ epic_id: tool.schema
1980
+ .string()
1981
+ .optional()
1982
+ .describe("Epic ID (for finalizing eval records when all subtasks complete)"),
1949
1983
  },
1950
1984
  async execute(args) {
1951
1985
  // Build outcome signals
@@ -1980,6 +2014,21 @@ export const swarm_record_outcome = tool({
1980
2014
  // Get error patterns from accumulator
1981
2015
  const errorStats = await globalErrorAccumulator.getErrorStats(args.bead_id);
1982
2016
 
2017
+ // Finalize eval record if project_path and epic_id provided
2018
+ let finalizedRecord: EvalRecord | null = null;
2019
+ if (args.project_path && args.epic_id) {
2020
+ try {
2021
+ const { finalizeEvalRecord } = await import("./eval-capture.js");
2022
+ finalizedRecord = finalizeEvalRecord({
2023
+ epicId: args.epic_id,
2024
+ projectPath: args.project_path,
2025
+ });
2026
+ } catch (error) {
2027
+ // Non-fatal - log and continue
2028
+ console.warn("[swarm_record_outcome] Failed to finalize eval record:", error);
2029
+ }
2030
+ }
2031
+
1983
2032
  // Generate feedback events for each criterion
1984
2033
  const criteriaToScore = args.criteria ?? [
1985
2034
  "type_safe",
@@ -2030,6 +2079,7 @@ export const swarm_record_outcome = tool({
2030
2079
  accumulated_errors: errorStats.total,
2031
2080
  unresolved_errors: errorStats.unresolved,
2032
2081
  },
2082
+ finalized_eval_record: finalizedRecord || undefined,
2033
2083
  note: "Feedback events should be stored for criterion weight calculation. Use learning.ts functions to apply weights.",
2034
2084
  },
2035
2085
  null,
@@ -2087,12 +2137,28 @@ export function extractTechStack(task: string): string[] {
2087
2137
  return Array.from(detected);
2088
2138
  }
2089
2139
 
2140
+ /**
2141
+ * Spawn instruction for a researcher worker
2142
+ */
2143
+ export interface ResearchSpawnInstruction {
2144
+ /** Unique ID for this research task */
2145
+ research_id: string;
2146
+ /** Technology being researched */
2147
+ tech: string;
2148
+ /** Full prompt for the researcher agent */
2149
+ prompt: string;
2150
+ /** Agent type for the Task tool */
2151
+ subagent_type: "swarm/researcher";
2152
+ }
2153
+
2090
2154
  /**
2091
2155
  * Research result from documentation discovery phase
2092
2156
  */
2093
2157
  export interface ResearchResult {
2094
2158
  /** Technologies identified and researched */
2095
2159
  tech_stack: string[];
2160
+ /** Spawn instructions for researcher workers */
2161
+ spawn_instructions: ResearchSpawnInstruction[];
2096
2162
  /** Summaries keyed by technology name */
2097
2163
  summaries: Record<string, string>;
2098
2164
  /** Semantic-memory IDs where research is stored */
@@ -2154,24 +2220,45 @@ export async function runResearchPhase(
2154
2220
  if (techStack.length === 0) {
2155
2221
  return {
2156
2222
  tech_stack: [],
2223
+ spawn_instructions: [],
2157
2224
  summaries: {},
2158
2225
  memory_ids: [],
2159
2226
  };
2160
2227
  }
2161
2228
 
2162
- // Step 2: For each technology, spawn a researcher
2163
- // TODO: Implement researcher spawning using swarm_spawn_researcher
2164
- // and Task tool. This requires coordination logic that will be
2165
- // added in a future iteration.
2166
-
2167
- // For now, return empty summaries (GREEN phase - make tests pass)
2168
- // The full implementation will spawn researchers in parallel and
2169
- // collect their findings.
2229
+ // Step 2: Generate spawn instructions for each technology
2230
+ // The coordinator will use these to spawn researcher workers via Task()
2231
+ const spawnInstructions: ResearchSpawnInstruction[] = [];
2232
+
2233
+ for (const tech of techStack) {
2234
+ // Generate unique research ID
2235
+ const researchId = `research-${tech}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
2236
+
2237
+ // Generate researcher prompt
2238
+ const prompt = formatResearcherPrompt({
2239
+ research_id: researchId,
2240
+ epic_id: "standalone-research", // No epic context for standalone research
2241
+ tech_stack: [tech], // Single tech per researcher
2242
+ project_path: projectPath,
2243
+ check_upgrades: options?.checkUpgrades ?? false,
2244
+ });
2245
+
2246
+ spawnInstructions.push({
2247
+ research_id: researchId,
2248
+ tech,
2249
+ prompt,
2250
+ subagent_type: "swarm/researcher",
2251
+ });
2252
+ }
2170
2253
 
2254
+ // Step 3: Return spawn instructions for coordinator
2255
+ // The coordinator will spawn Task() agents using these instructions
2256
+ // and collect results from swarm mail after completion
2171
2257
  return {
2172
2258
  tech_stack: techStack,
2173
- summaries: {},
2174
- memory_ids: [],
2259
+ spawn_instructions: spawnInstructions,
2260
+ summaries: {}, // Will be populated by coordinator after researchers complete
2261
+ memory_ids: [], // Will be populated by coordinator after researchers store in semantic-memory
2175
2262
  };
2176
2263
  }
2177
2264
 
@@ -9,8 +9,10 @@ import { describe, expect, test } from "bun:test";
9
9
  import {
10
10
  formatSubtaskPromptV2,
11
11
  formatResearcherPrompt,
12
+ formatCoordinatorPrompt,
12
13
  SUBTASK_PROMPT_V2,
13
14
  RESEARCHER_PROMPT,
15
+ COORDINATOR_PROMPT,
14
16
  } from "./swarm-prompts";
15
17
 
16
18
  describe("SUBTASK_PROMPT_V2", () => {
@@ -818,3 +820,122 @@ describe("swarm_spawn_retry tool", () => {
818
820
  expect(parsed.prompt).toMatch(/preserve.*working|fix.*while preserving/i);
819
821
  });
820
822
  });
823
+
824
+ describe("COORDINATOR_PROMPT", () => {
825
+ test("constant exists and is exported", () => {
826
+ expect(COORDINATOR_PROMPT).toBeDefined();
827
+ expect(typeof COORDINATOR_PROMPT).toBe("string");
828
+ expect(COORDINATOR_PROMPT.length).toBeGreaterThan(100);
829
+ });
830
+
831
+ test("contains all phase headers (0-8)", () => {
832
+ expect(COORDINATOR_PROMPT).toContain("Phase 0:");
833
+ expect(COORDINATOR_PROMPT).toContain("Phase 1:");
834
+ expect(COORDINATOR_PROMPT).toContain("Phase 2:");
835
+ expect(COORDINATOR_PROMPT).toContain("Phase 3:");
836
+ expect(COORDINATOR_PROMPT).toContain("Phase 4:");
837
+ expect(COORDINATOR_PROMPT).toContain("Phase 5:");
838
+ expect(COORDINATOR_PROMPT).toContain("Phase 6:");
839
+ expect(COORDINATOR_PROMPT).toContain("Phase 7:");
840
+ expect(COORDINATOR_PROMPT).toContain("Phase 8:");
841
+ });
842
+
843
+ test("contains Phase 1.5: Research Phase section", () => {
844
+ expect(COORDINATOR_PROMPT).toContain("Phase 1.5:");
845
+ expect(COORDINATOR_PROMPT).toMatch(/Phase 1\.5:.*Research/i);
846
+ });
847
+
848
+ test("Phase 1.5 documents swarm_spawn_researcher usage", () => {
849
+ // Extract Phase 1.5 section
850
+ const phase15Match = COORDINATOR_PROMPT.match(/Phase 1\.5:[\s\S]*?Phase 2:/);
851
+ expect(phase15Match).not.toBeNull();
852
+ if (!phase15Match) return;
853
+ const phase15Content = phase15Match[0];
854
+
855
+ expect(phase15Content).toContain("swarm_spawn_researcher");
856
+ expect(phase15Content).toContain("Task(subagent_type=\"swarm/researcher\"");
857
+ });
858
+
859
+ test("has section explicitly forbidding direct research tool calls", () => {
860
+ expect(COORDINATOR_PROMPT).toMatch(/NEVER.*direct|forbidden.*tools|do not call directly/i);
861
+ });
862
+
863
+ test("forbidden tools section lists all prohibited tools", () => {
864
+ const forbiddenTools = [
865
+ "repo-crawl_",
866
+ "repo-autopsy_",
867
+ "webfetch",
868
+ "fetch_fetch",
869
+ "context7_",
870
+ "pdf-brain_search",
871
+ "pdf-brain_read"
872
+ ];
873
+
874
+ for (const tool of forbiddenTools) {
875
+ expect(COORDINATOR_PROMPT).toContain(tool);
876
+ }
877
+ });
878
+
879
+ test("forbidden tools section explains to use swarm_spawn_researcher instead", () => {
880
+ // Find the forbidden tools section
881
+ const forbiddenMatch = COORDINATOR_PROMPT.match(/(FORBIDDEN.*for coordinators|NEVER.*FETCH.*DIRECTLY)[\s\S]{0,500}swarm_spawn_researcher/i);
882
+ expect(forbiddenMatch).not.toBeNull();
883
+ });
884
+
885
+ test("contains coordinator role boundaries section", () => {
886
+ expect(COORDINATOR_PROMPT).toContain("Coordinator Role Boundaries");
887
+ expect(COORDINATOR_PROMPT).toMatch(/COORDINATORS NEVER.*EXECUTE.*WORK/i);
888
+ });
889
+
890
+ test("contains MANDATORY review loop section", () => {
891
+ expect(COORDINATOR_PROMPT).toContain("MANDATORY Review Loop");
892
+ expect(COORDINATOR_PROMPT).toContain("swarm_review");
893
+ expect(COORDINATOR_PROMPT).toContain("swarm_review_feedback");
894
+ });
895
+
896
+ test("Phase 1.5 positioned between Phase 1 (Initialize) and Phase 2 (Knowledge)", () => {
897
+ const phase1Pos = COORDINATOR_PROMPT.indexOf("Phase 1:");
898
+ const phase15Pos = COORDINATOR_PROMPT.indexOf("Phase 1.5:");
899
+ const phase2Pos = COORDINATOR_PROMPT.indexOf("Phase 2:");
900
+
901
+ expect(phase15Pos).toBeGreaterThan(phase1Pos);
902
+ expect(phase15Pos).toBeLessThan(phase2Pos);
903
+ });
904
+ });
905
+
906
+ describe("formatCoordinatorPrompt", () => {
907
+ test("function exists and returns string", () => {
908
+ expect(formatCoordinatorPrompt).toBeDefined();
909
+ const result = formatCoordinatorPrompt({ task: "test task", projectPath: "/test" });
910
+ expect(typeof result).toBe("string");
911
+ });
912
+
913
+ test("substitutes {task} placeholder", () => {
914
+ const result = formatCoordinatorPrompt({
915
+ task: "Implement auth",
916
+ projectPath: "/test"
917
+ });
918
+ expect(result).toContain("Implement auth");
919
+ });
920
+
921
+ test("substitutes {project_path} placeholder", () => {
922
+ const result = formatCoordinatorPrompt({
923
+ task: "test",
924
+ projectPath: "/Users/joel/my-project"
925
+ });
926
+ expect(result).toContain("/Users/joel/my-project");
927
+ });
928
+
929
+ test("returns complete prompt with all phases", () => {
930
+ const result = formatCoordinatorPrompt({
931
+ task: "test",
932
+ projectPath: "/test"
933
+ });
934
+
935
+ // Should contain all phase headers
936
+ for (let i = 0; i <= 8; i++) {
937
+ expect(result).toContain(`Phase ${i}:`);
938
+ }
939
+ expect(result).toContain("Phase 1.5:");
940
+ });
941
+ });