@codemcp/workflows 4.10.0 → 4.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/components/beads/beads-instruction-generator.d.ts +3 -4
  3. package/dist/components/beads/beads-instruction-generator.d.ts.map +1 -1
  4. package/dist/components/beads/beads-instruction-generator.js +12 -7
  5. package/dist/components/beads/beads-instruction-generator.js.map +1 -1
  6. package/dist/components/beads/beads-task-backend-client.d.ts.map +1 -1
  7. package/dist/components/beads/beads-task-backend-client.js +1 -4
  8. package/dist/components/beads/beads-task-backend-client.js.map +1 -1
  9. package/dist/plugin-system/beads-plugin.d.ts +70 -0
  10. package/dist/plugin-system/beads-plugin.d.ts.map +1 -0
  11. package/dist/plugin-system/beads-plugin.js +459 -0
  12. package/dist/plugin-system/beads-plugin.js.map +1 -0
  13. package/dist/plugin-system/index.d.ts +9 -0
  14. package/dist/plugin-system/index.d.ts.map +1 -0
  15. package/dist/plugin-system/index.js +9 -0
  16. package/dist/plugin-system/index.js.map +1 -0
  17. package/dist/plugin-system/plugin-interfaces.d.ts +99 -0
  18. package/dist/plugin-system/plugin-interfaces.d.ts.map +1 -0
  19. package/dist/plugin-system/plugin-interfaces.js +9 -0
  20. package/dist/plugin-system/plugin-interfaces.js.map +1 -0
  21. package/dist/plugin-system/plugin-registry.d.ts +44 -0
  22. package/dist/plugin-system/plugin-registry.d.ts.map +1 -0
  23. package/dist/plugin-system/plugin-registry.js +132 -0
  24. package/dist/plugin-system/plugin-registry.js.map +1 -0
  25. package/dist/server-config.d.ts.map +1 -1
  26. package/dist/server-config.js +28 -8
  27. package/dist/server-config.js.map +1 -1
  28. package/dist/tool-handlers/conduct-review.d.ts.map +1 -1
  29. package/dist/tool-handlers/conduct-review.js +1 -2
  30. package/dist/tool-handlers/conduct-review.js.map +1 -1
  31. package/dist/tool-handlers/proceed-to-phase.d.ts +0 -5
  32. package/dist/tool-handlers/proceed-to-phase.d.ts.map +1 -1
  33. package/dist/tool-handlers/proceed-to-phase.js +15 -93
  34. package/dist/tool-handlers/proceed-to-phase.js.map +1 -1
  35. package/dist/tool-handlers/start-development.d.ts +0 -13
  36. package/dist/tool-handlers/start-development.d.ts.map +1 -1
  37. package/dist/tool-handlers/start-development.js +29 -124
  38. package/dist/tool-handlers/start-development.js.map +1 -1
  39. package/dist/tool-handlers/whats-next.d.ts.map +1 -1
  40. package/dist/tool-handlers/whats-next.js +1 -0
  41. package/dist/tool-handlers/whats-next.js.map +1 -1
  42. package/dist/types.d.ts +2 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +2 -2
  45. package/src/components/beads/beads-instruction-generator.ts +12 -12
  46. package/src/components/beads/beads-task-backend-client.ts +1 -4
  47. package/src/plugin-system/beads-plugin.ts +641 -0
  48. package/src/plugin-system/index.ts +20 -0
  49. package/src/plugin-system/plugin-interfaces.ts +154 -0
  50. package/src/plugin-system/plugin-registry.ts +190 -0
  51. package/src/server-config.ts +30 -8
  52. package/src/tool-handlers/conduct-review.ts +1 -2
  53. package/src/tool-handlers/proceed-to-phase.ts +19 -135
  54. package/src/tool-handlers/start-development.ts +35 -205
  55. package/src/tool-handlers/whats-next.ts +1 -0
  56. package/src/types.ts +2 -0
  57. package/test/e2e/beads-plugin-integration.test.ts +1609 -0
  58. package/test/e2e/plugin-system-integration.test.ts +1729 -0
  59. package/test/unit/beads-plugin-behavioral.test.ts +512 -0
  60. package/test/unit/beads-plugin.test.ts +94 -0
  61. package/test/unit/plugin-error-handling.test.ts +240 -0
  62. package/test/unit/proceed-to-phase-plugin-integration.test.ts +150 -0
  63. package/test/unit/server-config-plugin-registry.test.ts +81 -0
  64. package/test/unit/start-development-goal-extraction.test.ts +22 -16
  65. package/test/utils/test-helpers.ts +3 -1
  66. package/tsconfig.build.tsbuildinfo +1 -1
  67. package/dist/components/server-components-factory.d.ts +0 -39
  68. package/dist/components/server-components-factory.d.ts.map +0 -1
  69. package/dist/components/server-components-factory.js +0 -62
  70. package/dist/components/server-components-factory.js.map +0 -1
  71. package/src/components/server-components-factory.ts +0 -86
  72. package/test/e2e/component-substitution.test.ts +0 -208
  73. package/test/unit/beads-integration-filename.test.ts +0 -93
  74. package/test/unit/server-components-factory.test.ts +0 -279
@@ -0,0 +1,1609 @@
1
+ /**
2
+ * Comprehensive Beads Plugin Integration Test
3
+ *
4
+ * This single test file validates ALL aspects of beads plugin behavior:
5
+ * 1. Plan file structure with beads markers
6
+ * 2. Beads instruction generation
7
+ * 3. Beads task creation on start
8
+ * 4. Plan file task ID integration
9
+ * 5. Phase transition validation
10
+ * 6. With vs without beads comparison
11
+ * 7. Beads error handling
12
+ * 8. Plugin hook integration
13
+ *
14
+ * Design Principles:
15
+ * - NO fuzzy assertions
16
+ * - EXPLICIT validation of content (not just existence)
17
+ * - COMPREHENSIVE coverage of all beads functionality
18
+ * - PROPER isolation and cleanup between tests
19
+ * - MEANINGFUL test names that describe what is validated
20
+ */
21
+
22
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
23
+ import { createTempProjectWithDefaultStateMachine } from '../utils/temp-files';
24
+ import {
25
+ DirectServerInterface,
26
+ createSuiteIsolatedE2EScenario,
27
+ assertToolSuccess,
28
+ } from '../utils/e2e-test-setup';
29
+ import { promises as fs } from 'node:fs';
30
+ import { execSync } from 'node:child_process';
31
+ import type { StartDevelopmentResult } from '../../src/tool-handlers/start-development';
32
+ import type { WhatsNextResult } from '../../src/tool-handlers/whats-next';
33
+
34
+ vi.unmock('fs');
35
+ vi.unmock('fs/promises');
36
+
37
+ // Mock child_process to simulate beads CLI responses
38
+ vi.mock('node:child_process', () => ({
39
+ execSync: vi.fn(),
40
+ }));
41
+
42
+ // Track created task IDs for consistent mock responses
43
+ let mockTaskIdCounter = 0;
44
+
45
+ /**
46
+ * Setup beads CLI mock for tests
47
+ * This simulates the bd CLI responses needed for beads integration
48
+ */
49
+ function setupBeadsCliMock(): void {
50
+ mockTaskIdCounter = 0;
51
+
52
+ vi.mocked(execSync).mockImplementation((command: string) => {
53
+ // Handle bd --version check (used by TaskBackendManager)
54
+ if (command === 'bd --version') {
55
+ return 'beads v1.0.0\n';
56
+ }
57
+
58
+ // Handle bd list command (used to check if beads is initialized)
59
+ if (command.includes('bd list')) {
60
+ return ''; // Return empty list
61
+ }
62
+
63
+ // Handle bd init command
64
+ if (command.includes('bd init')) {
65
+ return 'Initialized beads repository\n';
66
+ }
67
+
68
+ // Handle bd create command for epic/phase tasks
69
+ if (command.includes('bd create')) {
70
+ mockTaskIdCounter++;
71
+ const taskId = `mock-task-${mockTaskIdCounter}`;
72
+ return `✓ Created issue: ${taskId}\n`;
73
+ }
74
+
75
+ // Handle bd dep command for dependencies
76
+ if (command.includes('bd dep')) {
77
+ return '✓ Dependency created\n';
78
+ }
79
+
80
+ // Handle bd show command
81
+ if (command.includes('bd show')) {
82
+ return 'Title: Mock Task\nStatus: open\n';
83
+ }
84
+
85
+ // Handle bd update command
86
+ if (command.includes('bd update')) {
87
+ return '✓ Updated\n';
88
+ }
89
+
90
+ // Handle bd close command
91
+ if (command.includes('bd close')) {
92
+ return '✓ Closed\n';
93
+ }
94
+
95
+ // Handle git commands (used in some tests)
96
+ if (command === 'git symbolic-ref --short HEAD') {
97
+ return 'feature/test-branch\n';
98
+ }
99
+
100
+ // For any other command, throw an error (unexpected command)
101
+ throw new Error(`Unexpected command in beads test: ${command}`);
102
+ });
103
+ }
104
+
105
+ // ============================================================================
106
+ // TEST CONSTANTS (Remove magic numbers)
107
+ // ============================================================================
108
+
109
+ // Minimum number of phases in a workflow that should have beads markers
110
+ const MIN_PHASES_WITH_MARKERS = 4;
111
+
112
+ // Minimum length for substantive instructions
113
+ // Must be long enough to contain meaningful guidance, not just placeholders
114
+ const MIN_INSTRUCTION_LENGTH = 200;
115
+
116
+ // ============================================================================
117
+ // HELPER FUNCTIONS
118
+ // ============================================================================
119
+
120
+ /**
121
+ * Verify beads plan file has the expected structure with markers
122
+ */
123
+ function validateBeadsPlanFileStructure(content: string): void {
124
+ // Should have beads-phase-id markers for each phase
125
+ expect(content).toContain('<!-- beads-phase-id:');
126
+
127
+ // Should have HTML comment format with either TBD or actual task IDs
128
+ // Task IDs can include alphanumerics, hyphens, and dots
129
+ expect(content).toMatch(/<!-- beads-phase-id:\s*(TBD|[a-zA-Z0-9\-.]+)\s*-->/);
130
+
131
+ // Should have phase headers
132
+ expect(content).toMatch(/^## \w+/m);
133
+
134
+ // Should have "Tasks managed via `bd` CLI" guidance
135
+ expect(content).toContain('Tasks managed via');
136
+ expect(content).toContain('bd');
137
+ }
138
+
139
+ /**
140
+ * Verify instructions contain beads CLI references
141
+ */
142
+ function validateBeadsInstructions(instructions: string): void {
143
+ // Should mention bd CLI tool
144
+ expect(instructions.toLowerCase()).toContain('bd');
145
+
146
+ // Should have bd commands
147
+ const hasCommands = /bd\s+(list|create|update|close|show)/i.test(
148
+ instructions
149
+ );
150
+ expect(hasCommands).toBe(true);
151
+
152
+ // Should mention task management
153
+ expect(instructions.toLowerCase()).toContain('task');
154
+
155
+ // Should have beads-specific guidance
156
+ expect(instructions).toContain('🔧 BD CLI Task Management');
157
+
158
+ // Should mention using ONLY bd CLI
159
+ expect(instructions).toContain('Use ONLY bd CLI tool');
160
+ }
161
+
162
+ /**
163
+ * Extract beads phase IDs from plan file content
164
+ * Returns array of task IDs found in the plan
165
+ */
166
+ function extractBeadsPhaseIds(content: string): string[] {
167
+ const matches =
168
+ content.match(/<!-- beads-phase-id:\s*([a-zA-Z0-9\-.]+)\s*-->/g) || [];
169
+ return matches
170
+ .map(match => {
171
+ const idMatch = match.match(/beads-phase-id:\s*([a-zA-Z0-9\-.]+)\s*-->/);
172
+ return idMatch ? idMatch[1] : '';
173
+ })
174
+ .filter(id => id.length > 0);
175
+ }
176
+
177
+ // ============================================================================
178
+ // TESTS
179
+ // ============================================================================
180
+
181
+ describe('Beads Plugin Comprehensive Integration', () => {
182
+ // =========================================================================
183
+ // 1. PLAN FILE STRUCTURE
184
+ // =========================================================================
185
+
186
+ describe('1. Plan File Structure with Beads Markers', () => {
187
+ let client: DirectServerInterface;
188
+ let cleanup: () => Promise<void>;
189
+
190
+ beforeEach(async () => {
191
+ // CRITICAL: Enable beads backend and mock CLI
192
+ process.env.TASK_BACKEND = 'beads';
193
+ setupBeadsCliMock();
194
+
195
+ const scenario = await createSuiteIsolatedE2EScenario({
196
+ suiteName: 'beads-plan-structure',
197
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
198
+ });
199
+ client = scenario.client;
200
+ cleanup = scenario.cleanup;
201
+ });
202
+
203
+ afterEach(async () => {
204
+ if (cleanup) {
205
+ await cleanup();
206
+ }
207
+ delete process.env.TASK_BACKEND;
208
+ });
209
+
210
+ it('should create plan file WITH beads-phase-id placeholders when TASK_BACKEND=beads', async () => {
211
+ // Verify environment
212
+ expect(process.env.TASK_BACKEND).toBe('beads');
213
+
214
+ // Start development
215
+ const result = await client.callTool('start_development', {
216
+ workflow: 'epcc',
217
+ commit_behaviour: 'none',
218
+ });
219
+
220
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
221
+ const planFilePath = response.plan_file_path;
222
+
223
+ // Read plan file
224
+ const planContent = await fs.readFile(planFilePath, 'utf-8');
225
+
226
+ // VALIDATE: Plan file has beads markers
227
+ validateBeadsPlanFileStructure(planContent);
228
+
229
+ // VALIDATE: Each phase has beads-phase-id placeholder
230
+ expect(planContent).toContain('## Explore');
231
+ expect(planContent).toMatch(/## Explore\n<!-- beads-phase-id:/);
232
+
233
+ expect(planContent).toContain('## Plan');
234
+ expect(planContent).toMatch(/## Plan\n<!-- beads-phase-id:/);
235
+
236
+ expect(planContent).toContain('## Code');
237
+ expect(planContent).toMatch(/## Code\n<!-- beads-phase-id:/);
238
+
239
+ expect(planContent).toContain('## Commit');
240
+ expect(planContent).toMatch(/## Commit\n<!-- beads-phase-id:/);
241
+ });
242
+
243
+ it('should have beads phase IDs for each phase (updated by plugin hook)', async () => {
244
+ // Start development
245
+ const result = await client.callTool('start_development', {
246
+ workflow: 'epcc',
247
+ commit_behaviour: 'none',
248
+ });
249
+
250
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
251
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
252
+
253
+ // VALIDATE: Plan has beads-phase-id markers (either TBD or actual IDs)
254
+ // The plugin's afterStartDevelopment hook will update TBD placeholders with actual task IDs
255
+ expect(planContent).toContain('<!-- beads-phase-id:');
256
+
257
+ // Count beads-phase-id occurrences (should be one per phase)
258
+ const phaseIdMatches = planContent.match(/<!-- beads-phase-id:/g);
259
+ expect(phaseIdMatches).not.toBeNull();
260
+ expect((phaseIdMatches || []).length).toBeGreaterThanOrEqual(
261
+ MIN_PHASES_WITH_MARKERS
262
+ ); // At least 4 phases
263
+ });
264
+
265
+ it('should format beads markers as HTML comments with proper structure', async () => {
266
+ // Start development
267
+ const result = await client.callTool('start_development', {
268
+ workflow: 'epcc',
269
+ commit_behaviour: 'none',
270
+ });
271
+
272
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
273
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
274
+
275
+ // VALIDATE: Format matches the pattern (either TBD or actual task IDs with dots)
276
+ expect(planContent).toMatch(
277
+ /## \w+\n<!-- beads-phase-id:\s*(TBD|[a-zA-Z0-9\-.]+)\s*-->\n### Tasks/
278
+ );
279
+
280
+ // VALIDATE: Must be HTML comment format
281
+ expect(planContent).toMatch(/<!-- beads-phase-id:/);
282
+ expect(planContent).not.toMatch(/\/\/ beads-phase-id:/);
283
+ });
284
+ });
285
+
286
+ // =========================================================================
287
+ // 2. BEADS INSTRUCTION GENERATION
288
+ // =========================================================================
289
+
290
+ describe('2. Beads Instruction Generation', () => {
291
+ let client: DirectServerInterface;
292
+ let cleanup: () => Promise<void>;
293
+
294
+ beforeEach(async () => {
295
+ process.env.TASK_BACKEND = 'beads';
296
+ setupBeadsCliMock();
297
+
298
+ const scenario = await createSuiteIsolatedE2EScenario({
299
+ suiteName: 'beads-instructions',
300
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
301
+ });
302
+ client = scenario.client;
303
+ cleanup = scenario.cleanup;
304
+ });
305
+
306
+ afterEach(async () => {
307
+ if (cleanup) {
308
+ await cleanup();
309
+ }
310
+ delete process.env.TASK_BACKEND;
311
+ });
312
+
313
+ it('should generate beads-specific instructions mentioning bd CLI', async () => {
314
+ // Start development
315
+ const startResult = await client.callTool('start_development', {
316
+ workflow: 'epcc',
317
+ commit_behaviour: 'none',
318
+ });
319
+ assertToolSuccess(startResult);
320
+
321
+ // Get instructions
322
+ const whatsNextResult = await client.callTool('whats_next', {
323
+ context: 'Testing beads instructions',
324
+ user_input: 'What should I do?',
325
+ conversation_summary: 'Started development with beads',
326
+ recent_messages: [],
327
+ });
328
+
329
+ const response = assertToolSuccess(whatsNextResult) as WhatsNextResult;
330
+ const instructions = response.instructions;
331
+
332
+ // VALIDATE: Instructions mention beads
333
+ validateBeadsInstructions(instructions);
334
+ });
335
+
336
+ it('should include bd CLI commands in instructions', async () => {
337
+ // Start development
338
+ const startResult = await client.callTool('start_development', {
339
+ workflow: 'epcc',
340
+ commit_behaviour: 'none',
341
+ });
342
+ assertToolSuccess(startResult);
343
+
344
+ // Get instructions
345
+ const whatsNextResult = await client.callTool('whats_next', {
346
+ context: 'Testing beads instructions',
347
+ user_input: 'What should I do?',
348
+ conversation_summary: 'Started development with beads',
349
+ recent_messages: [],
350
+ });
351
+
352
+ const response = assertToolSuccess(whatsNextResult) as WhatsNextResult;
353
+ const instructions = response.instructions;
354
+
355
+ // VALIDATE: Should have multiple bd commands
356
+ expect(instructions).toMatch(/`bd\s+list/);
357
+ expect(instructions).toMatch(/`bd\s+create/);
358
+ expect(instructions).toMatch(/`bd\s+(update|close)/);
359
+ });
360
+
361
+ it('should remind user to use ONLY bd CLI for task management', async () => {
362
+ // Start development
363
+ const startResult = await client.callTool('start_development', {
364
+ workflow: 'epcc',
365
+ commit_behaviour: 'none',
366
+ });
367
+ assertToolSuccess(startResult);
368
+
369
+ // Get instructions
370
+ const whatsNextResult = await client.callTool('whats_next', {
371
+ context: 'Testing beads instructions',
372
+ user_input: 'What should I do?',
373
+ conversation_summary: 'Started development with beads',
374
+ recent_messages: [],
375
+ });
376
+
377
+ const response = assertToolSuccess(whatsNextResult) as WhatsNextResult;
378
+ const instructions = response.instructions;
379
+
380
+ // VALIDATE: Clear instruction about bd CLI exclusivity
381
+ expect(instructions).toContain('Use ONLY bd CLI tool');
382
+ expect(instructions).toContain(
383
+ 'do not use your own task management tools'
384
+ );
385
+ });
386
+ });
387
+
388
+ // =========================================================================
389
+ // 3. WITH VS WITHOUT BEADS COMPARISON
390
+ // =========================================================================
391
+
392
+ describe('3. Plan File and Instructions: With vs Without Beads', () => {
393
+ it('should produce DIFFERENT plan files with and without beads', async () => {
394
+ // Create two independent scenarios
395
+ let cleanupWith: () => Promise<void>;
396
+ let cleanupWithout: () => Promise<void>;
397
+
398
+ // WITH BEADS
399
+ process.env.TASK_BACKEND = 'beads';
400
+ setupBeadsCliMock();
401
+ const scenarioWith = await createSuiteIsolatedE2EScenario({
402
+ suiteName: 'beads-comparison-with',
403
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
404
+ });
405
+ const clientWith = scenarioWith.client;
406
+ cleanupWith = scenarioWith.cleanup;
407
+
408
+ const resultWith = await clientWith.callTool('start_development', {
409
+ workflow: 'epcc',
410
+ commit_behaviour: 'none',
411
+ });
412
+ const responseWith = assertToolSuccess(
413
+ resultWith
414
+ ) as StartDevelopmentResult;
415
+ const planContentWith = await fs.readFile(
416
+ responseWith.plan_file_path,
417
+ 'utf-8'
418
+ );
419
+
420
+ await cleanupWith();
421
+ delete process.env.TASK_BACKEND;
422
+
423
+ // WITHOUT BEADS
424
+ const scenarioWithout = await createSuiteIsolatedE2EScenario({
425
+ suiteName: 'beads-comparison-without',
426
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
427
+ });
428
+ const clientWithout = scenarioWithout.client;
429
+ cleanupWithout = scenarioWithout.cleanup;
430
+
431
+ const resultWithout = await clientWithout.callTool('start_development', {
432
+ workflow: 'epcc',
433
+ commit_behaviour: 'none',
434
+ });
435
+ const responseWithout = assertToolSuccess(
436
+ resultWithout
437
+ ) as StartDevelopmentResult;
438
+ const planContentWithout = await fs.readFile(
439
+ responseWithout.plan_file_path,
440
+ 'utf-8'
441
+ );
442
+
443
+ await cleanupWithout();
444
+
445
+ // VALIDATE: WITH beads has beads markers
446
+ expect(planContentWith).toContain('<!-- beads-phase-id:');
447
+
448
+ // VALIDATE: WITHOUT beads does NOT have beads markers
449
+ expect(planContentWithout).not.toContain('<!-- beads-phase-id:');
450
+
451
+ // VALIDATE: WITHOUT beads uses checkbox format
452
+ expect(planContentWithout).toContain('- [ ]');
453
+ });
454
+
455
+ it('should generate DIFFERENT instructions with and without beads', async () => {
456
+ // WITH BEADS
457
+ process.env.TASK_BACKEND = 'beads';
458
+ setupBeadsCliMock();
459
+ const scenarioWith = await createSuiteIsolatedE2EScenario({
460
+ suiteName: 'beads-instructions-comparison-with',
461
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
462
+ });
463
+ const clientWith = scenarioWith.client;
464
+
465
+ await clientWith.callTool('start_development', {
466
+ workflow: 'epcc',
467
+ commit_behaviour: 'none',
468
+ });
469
+
470
+ const whatsNextWith = await clientWith.callTool('whats_next', {
471
+ context: 'Testing',
472
+ user_input: 'What should I do?',
473
+ conversation_summary: 'Started',
474
+ recent_messages: [],
475
+ });
476
+ const responseWith = assertToolSuccess(whatsNextWith) as WhatsNextResult;
477
+ const instructionsWithBeads = responseWith.instructions;
478
+
479
+ await scenarioWith.cleanup();
480
+ delete process.env.TASK_BACKEND;
481
+
482
+ // WITHOUT BEADS
483
+ const scenarioWithout = await createSuiteIsolatedE2EScenario({
484
+ suiteName: 'beads-instructions-comparison-without',
485
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
486
+ });
487
+ const clientWithout = scenarioWithout.client;
488
+
489
+ await clientWithout.callTool('start_development', {
490
+ workflow: 'epcc',
491
+ commit_behaviour: 'none',
492
+ });
493
+
494
+ const whatsNextWithout = await clientWithout.callTool('whats_next', {
495
+ context: 'Testing',
496
+ user_input: 'What should I do?',
497
+ conversation_summary: 'Started',
498
+ recent_messages: [],
499
+ });
500
+ const responseWithout = assertToolSuccess(
501
+ whatsNextWithout
502
+ ) as WhatsNextResult;
503
+ const instructionsWithout = responseWithout.instructions;
504
+
505
+ await scenarioWithout.cleanup();
506
+
507
+ // VALIDATE: WITH beads mentions bd CLI
508
+ expect(instructionsWithBeads.toLowerCase()).toContain('bd');
509
+ expect(instructionsWithBeads).toContain('BD CLI Task Management');
510
+
511
+ // VALIDATE: WITHOUT beads does NOT mention bd CLI
512
+ expect(instructionsWithout.toLowerCase()).not.toContain('bd cli');
513
+ expect(instructionsWithout).not.toContain('BD CLI Task Management');
514
+ });
515
+
516
+ it('should maintain identical response contracts regardless of beads', async () => {
517
+ // WITH BEADS
518
+ process.env.TASK_BACKEND = 'beads';
519
+ setupBeadsCliMock();
520
+ const scenarioWith = await createSuiteIsolatedE2EScenario({
521
+ suiteName: 'beads-contract-with',
522
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
523
+ });
524
+ const clientWith = scenarioWith.client;
525
+
526
+ const resultWith = await clientWith.callTool('start_development', {
527
+ workflow: 'epcc',
528
+ commit_behaviour: 'none',
529
+ });
530
+ const responseWith = assertToolSuccess(resultWith);
531
+
532
+ await scenarioWith.cleanup();
533
+ delete process.env.TASK_BACKEND;
534
+
535
+ // WITHOUT BEADS
536
+ const scenarioWithout = await createSuiteIsolatedE2EScenario({
537
+ suiteName: 'beads-contract-without',
538
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
539
+ });
540
+ const clientWithout = scenarioWithout.client;
541
+
542
+ const resultWithout = await clientWithout.callTool('start_development', {
543
+ workflow: 'epcc',
544
+ commit_behaviour: 'none',
545
+ });
546
+ const responseWithout = assertToolSuccess(resultWithout);
547
+
548
+ await scenarioWithout.cleanup();
549
+
550
+ // VALIDATE: Both responses have identical properties
551
+ expect(responseWith).toHaveProperty('conversation_id');
552
+ expect(responseWith).toHaveProperty('phase');
553
+ expect(responseWith).toHaveProperty('plan_file_path');
554
+ expect(responseWith).toHaveProperty('instructions');
555
+
556
+ expect(responseWithout).toHaveProperty('conversation_id');
557
+ expect(responseWithout).toHaveProperty('phase');
558
+ expect(responseWithout).toHaveProperty('plan_file_path');
559
+ expect(responseWithout).toHaveProperty('instructions');
560
+
561
+ // VALIDATE: Response structure identical
562
+ expect(Object.keys(responseWith).sort()).toEqual(
563
+ Object.keys(responseWithout).sort()
564
+ );
565
+ });
566
+ });
567
+
568
+ // =========================================================================
569
+ // 4. PLAN FILE TASK ID INTEGRATION
570
+ // =========================================================================
571
+
572
+ describe('4. Plan File Task ID Integration', () => {
573
+ let client: DirectServerInterface;
574
+ let cleanup: () => Promise<void>;
575
+
576
+ beforeEach(async () => {
577
+ process.env.TASK_BACKEND = 'beads';
578
+ setupBeadsCliMock();
579
+
580
+ const scenario = await createSuiteIsolatedE2EScenario({
581
+ suiteName: 'beads-task-ids',
582
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
583
+ });
584
+ client = scenario.client;
585
+ cleanup = scenario.cleanup;
586
+ });
587
+
588
+ afterEach(async () => {
589
+ if (cleanup) {
590
+ await cleanup();
591
+ }
592
+ delete process.env.TASK_BACKEND;
593
+ });
594
+
595
+ it('should have beads phase IDs after task creation by plugin hooks', async () => {
596
+ // Start development
597
+ const result = await client.callTool('start_development', {
598
+ workflow: 'epcc',
599
+ commit_behaviour: 'none',
600
+ });
601
+
602
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
603
+ const planFilePath = response.plan_file_path;
604
+ const planContent = await fs.readFile(planFilePath, 'utf-8');
605
+
606
+ // VALIDATE: Plan has beads markers (IDs updated by afterStartDevelopment hook)
607
+ expect(planContent).toContain('<!-- beads-phase-id:');
608
+
609
+ // VALIDATE: Has proper format with TBD or actual task IDs
610
+ expect(planContent).toMatch(
611
+ /## \w+\n<!-- beads-phase-id:\s*(TBD|[a-zA-Z0-9\-.]+)\s*-->\n### Tasks/
612
+ );
613
+ });
614
+
615
+ it('should have valid beads-phase-id format (not TBD after plugin execution)', async () => {
616
+ // Start development
617
+ const result = await client.callTool('start_development', {
618
+ workflow: 'epcc',
619
+ commit_behaviour: 'none',
620
+ });
621
+
622
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
623
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
624
+
625
+ // VALIDATE: Format must match - either TBD or actual task IDs with dots
626
+ const validFormats = planContent.match(
627
+ /<!-- beads-phase-id:\s*(TBD|[a-zA-Z0-9\-.]+)\s*-->/g
628
+ );
629
+ expect(validFormats).not.toBeNull();
630
+ expect((validFormats || []).length).toBeGreaterThanOrEqual(1);
631
+
632
+ // VALIDATE: No malformed placeholders
633
+ expect(planContent).not.toMatch(/<!-- beads-phase-id:\s*-->/);
634
+ });
635
+
636
+ it('should preserve plan file structure when updating task IDs', async () => {
637
+ // Start development
638
+ const result = await client.callTool('start_development', {
639
+ workflow: 'epcc',
640
+ commit_behaviour: 'none',
641
+ });
642
+
643
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
644
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
645
+
646
+ // VALIDATE: Plan structure intact
647
+ expect(planContent).toContain('# Development Plan:');
648
+ expect(planContent).toContain('## Goal');
649
+ expect(planContent).toContain('## Explore');
650
+ expect(planContent).toContain('## Plan');
651
+ expect(planContent).toContain('## Code');
652
+ expect(planContent).toContain('## Commit');
653
+ expect(planContent).toContain('## Key Decisions');
654
+ expect(planContent).toContain('## Notes');
655
+
656
+ // VALIDATE: Markdown is valid
657
+ expect(planContent).toMatch(/^# Development Plan:/m);
658
+ expect(planContent).toMatch(/^## /m);
659
+ });
660
+ });
661
+
662
+ // =========================================================================
663
+ // 5. ERROR HANDLING AND DEGRADATION
664
+ // =========================================================================
665
+
666
+ describe('5. Beads Error Handling and Graceful Degradation', () => {
667
+ let client: DirectServerInterface;
668
+ let cleanup: () => Promise<void>;
669
+
670
+ beforeEach(async () => {
671
+ process.env.TASK_BACKEND = 'beads';
672
+ setupBeadsCliMock();
673
+
674
+ const scenario = await createSuiteIsolatedE2EScenario({
675
+ suiteName: 'beads-error-handling',
676
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
677
+ });
678
+ client = scenario.client;
679
+ cleanup = scenario.cleanup;
680
+ });
681
+
682
+ afterEach(async () => {
683
+ if (cleanup) {
684
+ await cleanup();
685
+ }
686
+ delete process.env.TASK_BACKEND;
687
+ });
688
+
689
+ it('should create valid plan file even when beads unavailable', async () => {
690
+ // Start development with beads enabled
691
+ const result = await client.callTool('start_development', {
692
+ workflow: 'epcc',
693
+ commit_behaviour: 'none',
694
+ });
695
+
696
+ // Should NOT return error - graceful degradation
697
+ expect(result).not.toHaveProperty('error');
698
+
699
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
700
+
701
+ // VALIDATE: Plan file created successfully
702
+ expect(response.plan_file_path).toBeDefined();
703
+ expect(response.plan_file_path).toBeTruthy();
704
+
705
+ // VALIDATE: Plan file exists and is readable
706
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
707
+ expect(planContent).toBeTruthy();
708
+
709
+ // VALIDATE: Plan has beads markers even if tasks weren't created
710
+ expect(planContent).toContain('<!-- beads-phase-id:');
711
+ });
712
+
713
+ it('should return success response even if beads operations fail', async () => {
714
+ // Start development
715
+ const result = await client.callTool('start_development', {
716
+ workflow: 'epcc',
717
+ commit_behaviour: 'none',
718
+ });
719
+
720
+ // VALIDATE: Response is successful (no error field)
721
+ expect(result).not.toHaveProperty('error');
722
+
723
+ // VALIDATE: Has all required response fields
724
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
725
+ expect(response).toHaveProperty('conversation_id');
726
+ expect(response).toHaveProperty('phase');
727
+ expect(response).toHaveProperty('plan_file_path');
728
+ expect(response).toHaveProperty('instructions');
729
+ });
730
+ });
731
+
732
+ // =========================================================================
733
+ // 6. PLUGIN HOOK INTEGRATION
734
+ // =========================================================================
735
+
736
+ describe('6. Plugin Hook Integration', () => {
737
+ let client: DirectServerInterface;
738
+ let cleanup: () => Promise<void>;
739
+
740
+ beforeEach(async () => {
741
+ process.env.TASK_BACKEND = 'beads';
742
+ setupBeadsCliMock();
743
+
744
+ const scenario = await createSuiteIsolatedE2EScenario({
745
+ suiteName: 'beads-plugin-hooks',
746
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
747
+ });
748
+ client = scenario.client;
749
+ cleanup = scenario.cleanup;
750
+ });
751
+
752
+ afterEach(async () => {
753
+ if (cleanup) {
754
+ await cleanup();
755
+ }
756
+ delete process.env.TASK_BACKEND;
757
+ });
758
+
759
+ it('should execute plugin hooks during start_development', async () => {
760
+ // Start development - this should execute afterStartDevelopment hook
761
+ const result = await client.callTool('start_development', {
762
+ workflow: 'epcc',
763
+ commit_behaviour: 'none',
764
+ });
765
+
766
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
767
+
768
+ // VALIDATE: Plan file was created by plugin
769
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
770
+ expect(planContent).toContain('<!-- beads-phase-id:');
771
+
772
+ // VALIDATE: Response indicates successful hook execution
773
+ expect(response.plan_file_path).toBeTruthy();
774
+ expect(response.instructions).toBeTruthy();
775
+ });
776
+
777
+ it('should coordinate afterStartDevelopment and afterPlanFileCreated hooks', async () => {
778
+ // Start development
779
+ const result = await client.callTool('start_development', {
780
+ workflow: 'epcc',
781
+ commit_behaviour: 'none',
782
+ });
783
+
784
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
785
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
786
+
787
+ // VALIDATE: Plan file has beads structure (created by hooks)
788
+ expect(planContent).toContain('<!-- beads-phase-id:');
789
+
790
+ // VALIDATE: Plan has proper content from both hooks
791
+ expect(planContent).toContain('## Explore');
792
+ expect(planContent).toContain('## Plan');
793
+ expect(planContent).toContain('## Code');
794
+ expect(planContent).toContain('## Commit');
795
+
796
+ // VALIDATE: Each phase has a beads marker
797
+ const phaseMatches = planContent.match(/## \w+\n<!-- beads-phase-id:/g);
798
+ expect(phaseMatches).not.toBeNull();
799
+ expect((phaseMatches || []).length).toBeGreaterThanOrEqual(4);
800
+ });
801
+
802
+ it('should maintain system in consistent state after hook execution', async () => {
803
+ // Start development
804
+ const startResult = await client.callTool('start_development', {
805
+ workflow: 'epcc',
806
+ commit_behaviour: 'none',
807
+ });
808
+
809
+ const startResponse = assertToolSuccess(
810
+ startResult
811
+ ) as StartDevelopmentResult;
812
+
813
+ // Get current state
814
+ const whatsNextResult = await client.callTool('whats_next', {
815
+ context: 'Testing hook consistency',
816
+ user_input: 'What should I do?',
817
+ conversation_summary: 'Just started development',
818
+ recent_messages: [],
819
+ });
820
+
821
+ const whatsNextResponse = assertToolSuccess(
822
+ whatsNextResult
823
+ ) as WhatsNextResult;
824
+
825
+ // VALIDATE: State is consistent
826
+ expect(whatsNextResponse.phase).toBe('explore');
827
+ expect(whatsNextResponse.plan_file_path).toBe(
828
+ startResponse.plan_file_path
829
+ );
830
+ expect(whatsNextResponse.conversation_id).toBe(
831
+ startResponse.conversation_id
832
+ );
833
+
834
+ // VALIDATE: Plan file is still valid
835
+ const planContent = await fs.readFile(
836
+ whatsNextResponse.plan_file_path,
837
+ 'utf-8'
838
+ );
839
+ expect(planContent).toContain('<!-- beads-phase-id:');
840
+ });
841
+ });
842
+
843
+ // =========================================================================
844
+ // 7. BEADS ACTIVATION AND ENVIRONMENT CHECK
845
+ // =========================================================================
846
+
847
+ describe('7. Beads Environment Activation', () => {
848
+ it('should apply beads when TASK_BACKEND=beads is set', async () => {
849
+ process.env.TASK_BACKEND = 'beads';
850
+ setupBeadsCliMock();
851
+
852
+ const scenario = await createSuiteIsolatedE2EScenario({
853
+ suiteName: 'beads-activation-with',
854
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
855
+ });
856
+
857
+ const result = await scenario.client.callTool('start_development', {
858
+ workflow: 'epcc',
859
+ commit_behaviour: 'none',
860
+ });
861
+
862
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
863
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
864
+
865
+ await scenario.cleanup();
866
+ delete process.env.TASK_BACKEND;
867
+
868
+ // VALIDATE: Beads features enabled
869
+ expect(planContent).toContain('<!-- beads-phase-id:');
870
+ });
871
+
872
+ it('should NOT apply beads when TASK_BACKEND is not set', async () => {
873
+ // Ensure env var is NOT set
874
+ delete process.env.TASK_BACKEND;
875
+
876
+ const scenario = await createSuiteIsolatedE2EScenario({
877
+ suiteName: 'beads-activation-without',
878
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
879
+ });
880
+
881
+ const result = await scenario.client.callTool('start_development', {
882
+ workflow: 'epcc',
883
+ commit_behaviour: 'none',
884
+ });
885
+
886
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
887
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
888
+
889
+ await scenario.cleanup();
890
+
891
+ // VALIDATE: Beads features NOT enabled
892
+ expect(planContent).not.toContain('<!-- beads-phase-id:');
893
+ });
894
+
895
+ it('should NOT apply beads when TASK_BACKEND has different value', async () => {
896
+ process.env.TASK_BACKEND = 'other-backend';
897
+
898
+ const scenario = await createSuiteIsolatedE2EScenario({
899
+ suiteName: 'beads-activation-other',
900
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
901
+ });
902
+
903
+ const result = await scenario.client.callTool('start_development', {
904
+ workflow: 'epcc',
905
+ commit_behaviour: 'none',
906
+ });
907
+
908
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
909
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
910
+
911
+ await scenario.cleanup();
912
+ delete process.env.TASK_BACKEND;
913
+
914
+ // VALIDATE: Beads features NOT enabled
915
+ expect(planContent).not.toContain('<!-- beads-phase-id:');
916
+ });
917
+ });
918
+
919
+ // =========================================================================
920
+ // 8. CONTENT VALIDATION AND SEMANTIC CHECKS
921
+ // =========================================================================
922
+
923
+ describe('8. Beads Content Validation and Semantic Checks', () => {
924
+ let client: DirectServerInterface;
925
+ let cleanup: () => Promise<void>;
926
+
927
+ beforeEach(async () => {
928
+ process.env.TASK_BACKEND = 'beads';
929
+ setupBeadsCliMock();
930
+
931
+ const scenario = await createSuiteIsolatedE2EScenario({
932
+ suiteName: 'beads-content-validation',
933
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
934
+ });
935
+ client = scenario.client;
936
+ cleanup = scenario.cleanup;
937
+ });
938
+
939
+ afterEach(async () => {
940
+ if (cleanup) {
941
+ await cleanup();
942
+ }
943
+ delete process.env.TASK_BACKEND;
944
+ });
945
+
946
+ it('should generate substantive beads instructions with actual guidance', async () => {
947
+ // Start development
948
+ const startResult = await client.callTool('start_development', {
949
+ workflow: 'epcc',
950
+ commit_behaviour: 'none',
951
+ });
952
+ assertToolSuccess(startResult);
953
+
954
+ // Get instructions
955
+ const whatsNextResult = await client.callTool('whats_next', {
956
+ context: 'Testing instruction quality',
957
+ user_input: 'What should I do?',
958
+ conversation_summary: 'Started development',
959
+ recent_messages: [],
960
+ });
961
+
962
+ const response = assertToolSuccess(whatsNextResult) as WhatsNextResult;
963
+ const instructions = response.instructions;
964
+
965
+ // VALIDATE: Instructions are substantive (not just placeholders)
966
+ expect(instructions.length).toBeGreaterThan(MIN_INSTRUCTION_LENGTH);
967
+
968
+ // VALIDATE: Instructions contain beads-specific guidance
969
+ validateBeadsInstructions(instructions);
970
+
971
+ // VALIDATE: Instructions mention specific phases
972
+ expect(instructions).toMatch(/explore|plan|code|commit/i);
973
+ });
974
+
975
+ it('should create valid markdown plan file structure', async () => {
976
+ // Start development
977
+ const result = await client.callTool('start_development', {
978
+ workflow: 'epcc',
979
+ commit_behaviour: 'none',
980
+ });
981
+
982
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
983
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
984
+
985
+ // VALIDATE: Markdown structure
986
+ expect(planContent).toMatch(/^# /m); // Title
987
+ expect(planContent).toMatch(/^## /m); // Sections
988
+ expect(planContent).toMatch(/^### /m); // Subsections
989
+
990
+ // VALIDATE: No malformed headers
991
+ expect(planContent).not.toMatch(/^#$/m); // Empty header
992
+ expect(planContent).not.toMatch(/^## $/m); // Empty section
993
+
994
+ // VALIDATE: Beads markers are valid comments
995
+ expect(planContent).toMatch(/<!-- beads-phase-id:/);
996
+ expect(planContent).not.toMatch(/<!-- -->/); // Empty comment
997
+ });
998
+
999
+ it('should include beads CLI guidance in plan file', async () => {
1000
+ // Start development
1001
+ const result = await client.callTool('start_development', {
1002
+ workflow: 'epcc',
1003
+ commit_behaviour: 'none',
1004
+ });
1005
+
1006
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
1007
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
1008
+
1009
+ // VALIDATE: Plan mentions beads CLI
1010
+ expect(planContent).toContain('bd');
1011
+ expect(planContent).toContain('Tasks managed via');
1012
+ expect(planContent).toContain('beads CLI');
1013
+ });
1014
+ });
1015
+
1016
+ // =========================================================================
1017
+ // 9. TASK ID EXTRACTION AND VALIDATION
1018
+ // =========================================================================
1019
+
1020
+ describe('9. Task ID Extraction and Validation', () => {
1021
+ let client: DirectServerInterface;
1022
+ let cleanup: () => Promise<void>;
1023
+
1024
+ beforeEach(async () => {
1025
+ process.env.TASK_BACKEND = 'beads';
1026
+ setupBeadsCliMock();
1027
+
1028
+ const scenario = await createSuiteIsolatedE2EScenario({
1029
+ suiteName: 'beads-task-id-extraction',
1030
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1031
+ });
1032
+ client = scenario.client;
1033
+ cleanup = scenario.cleanup;
1034
+ });
1035
+
1036
+ afterEach(async () => {
1037
+ if (cleanup) {
1038
+ await cleanup();
1039
+ }
1040
+ delete process.env.TASK_BACKEND;
1041
+ });
1042
+
1043
+ it('should extract beads phase IDs from plan file', async () => {
1044
+ // Start development
1045
+ const result = await client.callTool('start_development', {
1046
+ workflow: 'epcc',
1047
+ commit_behaviour: 'none',
1048
+ });
1049
+
1050
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
1051
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
1052
+
1053
+ // VALIDATE: Extract IDs and verify format
1054
+ const extractedIds = extractBeadsPhaseIds(planContent);
1055
+
1056
+ // VALIDATE: Should have extracted some IDs
1057
+ expect(extractedIds).toBeDefined();
1058
+ expect(Array.isArray(extractedIds)).toBe(true);
1059
+
1060
+ // VALIDATE: Each ID should be a non-empty string
1061
+ for (const id of extractedIds) {
1062
+ expect(typeof id).toBe('string');
1063
+ expect(id.length).toBeGreaterThan(0);
1064
+ }
1065
+ });
1066
+
1067
+ it('should validate beads phase ID format', async () => {
1068
+ // Start development
1069
+ const result = await client.callTool('start_development', {
1070
+ workflow: 'epcc',
1071
+ commit_behaviour: 'none',
1072
+ });
1073
+
1074
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
1075
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
1076
+
1077
+ // VALIDATE: IDs must match pattern (alphanumeric, hyphens, dots)
1078
+ const allMatches = planContent.match(
1079
+ /<!-- beads-phase-id:\s*([a-zA-Z0-9\-.]+)\s*-->/g
1080
+ );
1081
+ expect(allMatches).not.toBeNull();
1082
+ expect((allMatches || []).length).toBeGreaterThan(0);
1083
+
1084
+ // VALIDATE: Each match is properly formatted
1085
+ for (const match of allMatches || []) {
1086
+ expect(match).toMatch(/^<!-- beads-phase-id:/);
1087
+ expect(match).toMatch(/-->$/);
1088
+ }
1089
+ });
1090
+
1091
+ it('should not have empty beads-phase-id placeholders', async () => {
1092
+ // Start development
1093
+ const result = await client.callTool('start_development', {
1094
+ workflow: 'epcc',
1095
+ commit_behaviour: 'none',
1096
+ });
1097
+
1098
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
1099
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
1100
+
1101
+ // VALIDATE: No malformed empty placeholders
1102
+ expect(planContent).not.toMatch(/<!-- beads-phase-id:\s*-->/);
1103
+ expect(planContent).not.toMatch(/<!-- beads-phase-id: -->/);
1104
+ });
1105
+
1106
+ it('should replace TBD placeholders with actual task IDs or keep TBD', async () => {
1107
+ // Start development
1108
+ const result = await client.callTool('start_development', {
1109
+ workflow: 'epcc',
1110
+ commit_behaviour: 'none',
1111
+ });
1112
+
1113
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
1114
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
1115
+
1116
+ // VALIDATE: All placeholders are either TBD or actual IDs (not empty)
1117
+ const placeholders = planContent.match(
1118
+ /<!-- beads-phase-id:\s*(TBD|[a-zA-Z0-9\-.]+)\s*-->/g
1119
+ );
1120
+ expect(placeholders).not.toBeNull();
1121
+
1122
+ for (const placeholder of placeholders || []) {
1123
+ // Each must have either TBD or an actual ID
1124
+ expect(placeholder).toMatch(/TBD|[a-zA-Z0-9\-.]+/);
1125
+ }
1126
+ });
1127
+ });
1128
+
1129
+ // =========================================================================
1130
+ // 10. PHASE TRANSITION AND TASK COMPLETION VALIDATION
1131
+ // =========================================================================
1132
+
1133
+ describe('10. Phase Transition and Task Completion', () => {
1134
+ let client: DirectServerInterface;
1135
+ let cleanup: () => Promise<void>;
1136
+
1137
+ beforeEach(async () => {
1138
+ process.env.TASK_BACKEND = 'beads';
1139
+ setupBeadsCliMock();
1140
+
1141
+ const scenario = await createSuiteIsolatedE2EScenario({
1142
+ suiteName: 'beads-phase-transitions',
1143
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1144
+ });
1145
+ client = scenario.client;
1146
+ cleanup = scenario.cleanup;
1147
+ });
1148
+
1149
+ afterEach(async () => {
1150
+ if (cleanup) {
1151
+ await cleanup();
1152
+ }
1153
+ delete process.env.TASK_BACKEND;
1154
+ });
1155
+
1156
+ it('should maintain beads markers through phase transitions', async () => {
1157
+ // Start development
1158
+ const startResult = await client.callTool('start_development', {
1159
+ workflow: 'epcc',
1160
+ commit_behaviour: 'none',
1161
+ });
1162
+
1163
+ const startResponse = assertToolSuccess(
1164
+ startResult
1165
+ ) as StartDevelopmentResult;
1166
+ let planContent = await fs.readFile(
1167
+ startResponse.plan_file_path,
1168
+ 'utf-8'
1169
+ );
1170
+ const initialMarkers = planContent.match(/<!-- beads-phase-id:/g);
1171
+
1172
+ // VALIDATE: Initial markers exist
1173
+ expect(initialMarkers).not.toBeNull();
1174
+ expect((initialMarkers || []).length).toBeGreaterThanOrEqual(
1175
+ MIN_PHASES_WITH_MARKERS
1176
+ );
1177
+
1178
+ // Transition to next phase
1179
+ const transitionResult = await client.callTool('proceed_to_phase', {
1180
+ target_phase: 'plan',
1181
+ reason: 'exploration complete',
1182
+ review_state: 'not-required',
1183
+ });
1184
+
1185
+ expect(transitionResult).not.toHaveProperty('error');
1186
+
1187
+ // VALIDATE: Markers still present after transition
1188
+ planContent = await fs.readFile(startResponse.plan_file_path, 'utf-8');
1189
+ const afterTransitionMarkers = planContent.match(/<!-- beads-phase-id:/g);
1190
+
1191
+ expect(afterTransitionMarkers).not.toBeNull();
1192
+ expect(afterTransitionMarkers).toEqual(initialMarkers);
1193
+ });
1194
+
1195
+ it('should preserve plan file structure across multiple phase transitions', async () => {
1196
+ // Start development
1197
+ const result = await client.callTool('start_development', {
1198
+ workflow: 'epcc',
1199
+ commit_behaviour: 'none',
1200
+ });
1201
+
1202
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
1203
+ const initialContent = await fs.readFile(
1204
+ response.plan_file_path,
1205
+ 'utf-8'
1206
+ );
1207
+
1208
+ // Verify initial structure
1209
+ expect(initialContent).toContain('## Explore');
1210
+ expect(initialContent).toContain('## Plan');
1211
+
1212
+ // Transition through phases
1213
+ await client.callTool('proceed_to_phase', {
1214
+ target_phase: 'plan',
1215
+ reason: 'exploration complete',
1216
+ review_state: 'not-required',
1217
+ });
1218
+
1219
+ // VALIDATE: Structure preserved
1220
+ const afterFirstTransition = await fs.readFile(
1221
+ response.plan_file_path,
1222
+ 'utf-8'
1223
+ );
1224
+ expect(afterFirstTransition).toContain('## Explore');
1225
+ expect(afterFirstTransition).toContain('## Plan');
1226
+ expect(afterFirstTransition).toContain('## Code');
1227
+ });
1228
+
1229
+ it('should keep beads markers consistent with phase structure', async () => {
1230
+ // Start development
1231
+ const result = await client.callTool('start_development', {
1232
+ workflow: 'epcc',
1233
+ commit_behaviour: 'none',
1234
+ });
1235
+
1236
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
1237
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
1238
+
1239
+ // VALIDATE: Each phase header has a corresponding beads marker
1240
+ const phasePattern = /## (\w+)\n<!-- beads-phase-id:/g;
1241
+ const phaseMatches: string[] = [];
1242
+ let match: RegExpExecArray | null;
1243
+ while ((match = phasePattern.exec(planContent)) !== null) {
1244
+ if (match[1]) {
1245
+ phaseMatches.push(match[1]);
1246
+ }
1247
+ }
1248
+
1249
+ // VALIDATE: Found multiple phases with markers
1250
+ expect(phaseMatches.length).toBeGreaterThanOrEqual(
1251
+ MIN_PHASES_WITH_MARKERS
1252
+ );
1253
+
1254
+ // VALIDATE: Phases are expected workflow phases
1255
+ const expectedPhases = ['Explore', 'Plan', 'Code', 'Commit'];
1256
+ for (const phase of phaseMatches) {
1257
+ expect(expectedPhases).toContain(phase);
1258
+ }
1259
+ });
1260
+ });
1261
+
1262
+ // =========================================================================
1263
+ // 11. PLUGIN HOOK EXECUTION AND SIDE EFFECTS
1264
+ // =========================================================================
1265
+
1266
+ describe('11. Plugin Hook Execution and Side Effects', () => {
1267
+ let client: DirectServerInterface;
1268
+ let cleanup: () => Promise<void>;
1269
+
1270
+ beforeEach(async () => {
1271
+ process.env.TASK_BACKEND = 'beads';
1272
+ setupBeadsCliMock();
1273
+
1274
+ const scenario = await createSuiteIsolatedE2EScenario({
1275
+ suiteName: 'beads-hook-execution',
1276
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1277
+ });
1278
+ client = scenario.client;
1279
+ cleanup = scenario.cleanup;
1280
+ });
1281
+
1282
+ afterEach(async () => {
1283
+ if (cleanup) {
1284
+ await cleanup();
1285
+ }
1286
+ delete process.env.TASK_BACKEND;
1287
+ });
1288
+
1289
+ it('should execute hooks and update plan file with markers', async () => {
1290
+ // Start development - triggers afterStartDevelopment hook
1291
+ const result = await client.callTool('start_development', {
1292
+ workflow: 'epcc',
1293
+ commit_behaviour: 'none',
1294
+ });
1295
+
1296
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
1297
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
1298
+
1299
+ // VALIDATE: Hook executed and updated plan file
1300
+ expect(planContent).toContain('<!-- beads-phase-id:');
1301
+
1302
+ // VALIDATE: Plan has beads-specific guidance
1303
+ expect(planContent).toContain('Tasks managed via');
1304
+ expect(planContent).toContain('bd');
1305
+ });
1306
+
1307
+ it('should ensure whats_next respects beads plan markers', async () => {
1308
+ // Start development
1309
+ const startResult = await client.callTool('start_development', {
1310
+ workflow: 'epcc',
1311
+ commit_behaviour: 'none',
1312
+ });
1313
+
1314
+ assertToolSuccess(startResult) as StartDevelopmentResult;
1315
+
1316
+ // Get whats_next guidance
1317
+ const whatsNextResult = await client.callTool('whats_next', {
1318
+ context: 'Continuing development',
1319
+ user_input: 'What should I do?',
1320
+ conversation_summary: 'Started with beads',
1321
+ recent_messages: [],
1322
+ });
1323
+
1324
+ const whatsNextResponse = assertToolSuccess(
1325
+ whatsNextResult
1326
+ ) as WhatsNextResult;
1327
+
1328
+ // VALIDATE: Instructions include beads guidance
1329
+ expect(whatsNextResponse.instructions).toContain('bd');
1330
+ expect(whatsNextResponse.instructions).toContain('Use ONLY bd CLI tool');
1331
+
1332
+ // VALIDATE: Plan file still has markers
1333
+ const planContent = await fs.readFile(
1334
+ whatsNextResponse.plan_file_path,
1335
+ 'utf-8'
1336
+ );
1337
+ expect(planContent).toContain('<!-- beads-phase-id:');
1338
+ });
1339
+
1340
+ it('should maintain conversation state through plugin hook execution', async () => {
1341
+ // Start development
1342
+ const startResult = await client.callTool('start_development', {
1343
+ workflow: 'epcc',
1344
+ commit_behaviour: 'none',
1345
+ });
1346
+
1347
+ const startResponse = assertToolSuccess(
1348
+ startResult
1349
+ ) as StartDevelopmentResult;
1350
+ const conversationId = startResponse.conversation_id;
1351
+
1352
+ // Get whats_next
1353
+ const whatsNextResult = await client.callTool('whats_next', {
1354
+ context: 'Testing conversation preservation',
1355
+ user_input: 'What is the current state?',
1356
+ conversation_summary: 'Started development',
1357
+ recent_messages: [],
1358
+ });
1359
+
1360
+ const whatsNextResponse = assertToolSuccess(
1361
+ whatsNextResult
1362
+ ) as WhatsNextResult;
1363
+
1364
+ // VALIDATE: Conversation ID preserved after hook execution
1365
+ expect(whatsNextResponse.conversation_id).toBe(conversationId);
1366
+
1367
+ // VALIDATE: Phase information consistent
1368
+ expect(whatsNextResponse.phase).toBe('explore');
1369
+ });
1370
+ });
1371
+
1372
+ // =========================================================================
1373
+ // 12. MULTI-WORKFLOW BEADS SUPPORT
1374
+ // =========================================================================
1375
+
1376
+ describe('12. Multi-Workflow Beads Support', () => {
1377
+ let cleanup: () => Promise<void>;
1378
+
1379
+ afterEach(async () => {
1380
+ if (cleanup) {
1381
+ await cleanup();
1382
+ }
1383
+ });
1384
+
1385
+ it('should apply beads markers to waterfall workflow', async () => {
1386
+ process.env.TASK_BACKEND = 'beads';
1387
+ setupBeadsCliMock();
1388
+
1389
+ const scenario = await createSuiteIsolatedE2EScenario({
1390
+ suiteName: 'beads-waterfall',
1391
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1392
+ });
1393
+ cleanup = scenario.cleanup;
1394
+
1395
+ const result = await scenario.client.callTool('start_development', {
1396
+ workflow: 'waterfall',
1397
+ commit_behaviour: 'none',
1398
+ });
1399
+
1400
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
1401
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
1402
+
1403
+ // VALIDATE: Beads markers present in waterfall
1404
+ expect(planContent).toContain('<!-- beads-phase-id:');
1405
+
1406
+ // VALIDATE: Waterfall phases have markers
1407
+ const phases = ['Requirements', 'Design', 'Implementation'];
1408
+ for (const phase of phases) {
1409
+ expect(planContent).toContain(`## ${phase}`);
1410
+ }
1411
+
1412
+ delete process.env.TASK_BACKEND;
1413
+ });
1414
+
1415
+ it('should apply beads markers to tdd workflow', async () => {
1416
+ process.env.TASK_BACKEND = 'beads';
1417
+ setupBeadsCliMock();
1418
+
1419
+ const scenario = await createSuiteIsolatedE2EScenario({
1420
+ suiteName: 'beads-tdd',
1421
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1422
+ });
1423
+ cleanup = scenario.cleanup;
1424
+
1425
+ const result = await scenario.client.callTool('start_development', {
1426
+ workflow: 'tdd',
1427
+ commit_behaviour: 'none',
1428
+ });
1429
+
1430
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
1431
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
1432
+
1433
+ // VALIDATE: Beads markers present in tdd
1434
+ expect(planContent).toContain('<!-- beads-phase-id:');
1435
+
1436
+ // VALIDATE: TDD phases have markers
1437
+ const phaseMatches = planContent.match(/## \w+\n<!-- beads-phase-id:/g);
1438
+ expect(phaseMatches).not.toBeNull();
1439
+ expect((phaseMatches || []).length).toBeGreaterThanOrEqual(3);
1440
+
1441
+ delete process.env.TASK_BACKEND;
1442
+ });
1443
+
1444
+ it('should apply beads markers to bugfix workflow', async () => {
1445
+ process.env.TASK_BACKEND = 'beads';
1446
+ setupBeadsCliMock();
1447
+
1448
+ const scenario = await createSuiteIsolatedE2EScenario({
1449
+ suiteName: 'beads-bugfix',
1450
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1451
+ });
1452
+ cleanup = scenario.cleanup;
1453
+
1454
+ const result = await scenario.client.callTool('start_development', {
1455
+ workflow: 'bugfix',
1456
+ commit_behaviour: 'none',
1457
+ });
1458
+
1459
+ const response = assertToolSuccess(result) as StartDevelopmentResult;
1460
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
1461
+
1462
+ // VALIDATE: Beads markers present in bugfix
1463
+ expect(planContent).toContain('<!-- beads-phase-id:');
1464
+
1465
+ // VALIDATE: Bugfix phases have markers
1466
+ const phaseMatches = planContent.match(/## \w+\n<!-- beads-phase-id:/g);
1467
+ expect(phaseMatches).not.toBeNull();
1468
+ expect((phaseMatches || []).length).toBeGreaterThanOrEqual(2);
1469
+
1470
+ delete process.env.TASK_BACKEND;
1471
+ });
1472
+ });
1473
+
1474
+ // =========================================================================
1475
+ // 13. BEADS INTEGRATION ROBUSTNESS
1476
+ // =========================================================================
1477
+
1478
+ describe('13. Beads Integration Robustness', () => {
1479
+ let client: DirectServerInterface;
1480
+ let cleanup: () => Promise<void>;
1481
+
1482
+ beforeEach(async () => {
1483
+ process.env.TASK_BACKEND = 'beads';
1484
+ setupBeadsCliMock();
1485
+
1486
+ const scenario = await createSuiteIsolatedE2EScenario({
1487
+ suiteName: 'beads-robustness',
1488
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1489
+ });
1490
+ client = scenario.client;
1491
+ cleanup = scenario.cleanup;
1492
+ });
1493
+
1494
+ afterEach(async () => {
1495
+ if (cleanup) {
1496
+ await cleanup();
1497
+ }
1498
+ delete process.env.TASK_BACKEND;
1499
+ });
1500
+
1501
+ it('should not break normal functionality when beads enabled', async () => {
1502
+ // Start development
1503
+ const startResult = await client.callTool('start_development', {
1504
+ workflow: 'epcc',
1505
+ commit_behaviour: 'none',
1506
+ });
1507
+
1508
+ const startResponse = assertToolSuccess(
1509
+ startResult
1510
+ ) as StartDevelopmentResult;
1511
+
1512
+ // VALIDATE: All standard response properties present
1513
+ expect(startResponse).toHaveProperty('conversation_id');
1514
+ expect(startResponse).toHaveProperty('phase');
1515
+ expect(startResponse).toHaveProperty('plan_file_path');
1516
+ expect(startResponse).toHaveProperty('instructions');
1517
+ expect(startResponse).toHaveProperty('workflow');
1518
+
1519
+ // VALIDATE: Can transition phases normally
1520
+ const transitionResult = await client.callTool('proceed_to_phase', {
1521
+ target_phase: 'plan',
1522
+ reason: 'exploration complete',
1523
+ review_state: 'not-required',
1524
+ });
1525
+
1526
+ expect(transitionResult).not.toHaveProperty('error');
1527
+
1528
+ // VALIDATE: Can get whats_next normally
1529
+ const whatsNextResult = await client.callTool('whats_next', {
1530
+ context: 'Testing',
1531
+ user_input: 'What now?',
1532
+ conversation_summary: 'Testing beads',
1533
+ recent_messages: [],
1534
+ });
1535
+
1536
+ expect(whatsNextResult).not.toHaveProperty('error');
1537
+ });
1538
+
1539
+ it('should have consistent beads markers across all workflow operations', async () => {
1540
+ // Start
1541
+ const startResult = await client.callTool('start_development', {
1542
+ workflow: 'epcc',
1543
+ commit_behaviour: 'none',
1544
+ });
1545
+
1546
+ const startResponse = assertToolSuccess(
1547
+ startResult
1548
+ ) as StartDevelopmentResult;
1549
+ let planContent = await fs.readFile(
1550
+ startResponse.plan_file_path,
1551
+ 'utf-8'
1552
+ );
1553
+ const startMarkers = planContent.match(/<!-- beads-phase-id:/g);
1554
+
1555
+ // Get whats_next
1556
+ const whatsNextResult = await client.callTool('whats_next', {
1557
+ context: 'Checking markers',
1558
+ user_input: 'What should I do?',
1559
+ conversation_summary: 'Just started',
1560
+ recent_messages: [],
1561
+ });
1562
+
1563
+ expect(whatsNextResult).not.toHaveProperty('error');
1564
+
1565
+ // Transition
1566
+ await client.callTool('proceed_to_phase', {
1567
+ target_phase: 'plan',
1568
+ reason: 'ready',
1569
+ review_state: 'not-required',
1570
+ });
1571
+
1572
+ planContent = await fs.readFile(startResponse.plan_file_path, 'utf-8');
1573
+ const transitionMarkers = planContent.match(/<!-- beads-phase-id:/g);
1574
+
1575
+ // VALIDATE: Markers consistent
1576
+ expect(startMarkers).toEqual(transitionMarkers);
1577
+ });
1578
+
1579
+ it('should generate consistent beads instructions across operations', async () => {
1580
+ // Start development
1581
+ const startResult = await client.callTool('start_development', {
1582
+ workflow: 'epcc',
1583
+ commit_behaviour: 'none',
1584
+ });
1585
+
1586
+ assertToolSuccess(startResult);
1587
+
1588
+ // Get instructions in whats_next
1589
+ const whatsNextResult = await client.callTool('whats_next', {
1590
+ context: 'Testing instruction consistency',
1591
+ user_input: 'What should I do?',
1592
+ conversation_summary: 'Started',
1593
+ recent_messages: [],
1594
+ });
1595
+
1596
+ const whatsNextResponse = assertToolSuccess(
1597
+ whatsNextResult
1598
+ ) as WhatsNextResult;
1599
+
1600
+ // VALIDATE: Instructions have beads content
1601
+ validateBeadsInstructions(whatsNextResponse.instructions);
1602
+
1603
+ // VALIDATE: Instructions contain expected guidance
1604
+ expect(whatsNextResponse.instructions).toContain('bd');
1605
+ expect(whatsNextResponse.instructions).toContain('Task');
1606
+ expect(whatsNextResponse.instructions).toContain('Use ONLY bd CLI tool');
1607
+ });
1608
+ });
1609
+ });