opencode-conductor-cdd-plugin 1.0.0-beta.13

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 (91) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +163 -0
  3. package/README.test.md +51 -0
  4. package/dist/commands/implement.d.ts +1 -0
  5. package/dist/commands/implement.js +30 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +108 -0
  8. package/dist/index.test.d.ts +1 -0
  9. package/dist/index.test.js +122 -0
  10. package/dist/prompts/agent/cdd.md +41 -0
  11. package/dist/prompts/agent/implementer.md +22 -0
  12. package/dist/prompts/agent.md +23 -0
  13. package/dist/prompts/cdd/implement.json +4 -0
  14. package/dist/prompts/cdd/newTrack.json +4 -0
  15. package/dist/prompts/cdd/revert.json +4 -0
  16. package/dist/prompts/cdd/setup.json +4 -0
  17. package/dist/prompts/cdd/setup.test.d.ts +1 -0
  18. package/dist/prompts/cdd/setup.test.js +132 -0
  19. package/dist/prompts/cdd/setup.test.ts +168 -0
  20. package/dist/prompts/cdd/status.json +4 -0
  21. package/dist/prompts/strategies/delegate.md +11 -0
  22. package/dist/prompts/strategies/manual.md +9 -0
  23. package/dist/templates/code_styleguides/c.md +28 -0
  24. package/dist/templates/code_styleguides/cpp.md +46 -0
  25. package/dist/templates/code_styleguides/csharp.md +115 -0
  26. package/dist/templates/code_styleguides/dart.md +238 -0
  27. package/dist/templates/code_styleguides/general.md +23 -0
  28. package/dist/templates/code_styleguides/go.md +48 -0
  29. package/dist/templates/code_styleguides/html-css.md +49 -0
  30. package/dist/templates/code_styleguides/java.md +39 -0
  31. package/dist/templates/code_styleguides/javascript.md +51 -0
  32. package/dist/templates/code_styleguides/julia.md +27 -0
  33. package/dist/templates/code_styleguides/kotlin.md +41 -0
  34. package/dist/templates/code_styleguides/php.md +37 -0
  35. package/dist/templates/code_styleguides/python.md +37 -0
  36. package/dist/templates/code_styleguides/react.md +37 -0
  37. package/dist/templates/code_styleguides/ruby.md +39 -0
  38. package/dist/templates/code_styleguides/rust.md +44 -0
  39. package/dist/templates/code_styleguides/shell.md +35 -0
  40. package/dist/templates/code_styleguides/solidity.md +60 -0
  41. package/dist/templates/code_styleguides/sql.md +39 -0
  42. package/dist/templates/code_styleguides/swift.md +36 -0
  43. package/dist/templates/code_styleguides/typescript.md +43 -0
  44. package/dist/templates/code_styleguides/vue.md +38 -0
  45. package/dist/templates/code_styleguides/zig.md +27 -0
  46. package/dist/templates/workflow.md +336 -0
  47. package/dist/tools/background.d.ts +54 -0
  48. package/dist/tools/background.js +198 -0
  49. package/dist/tools/commands.d.ts +11 -0
  50. package/dist/tools/commands.js +80 -0
  51. package/dist/tools/commands.test.d.ts +1 -0
  52. package/dist/tools/commands.test.js +142 -0
  53. package/dist/tools/delegate.d.ts +3 -0
  54. package/dist/tools/delegate.js +45 -0
  55. package/dist/utils/autogenerateFlow.d.ts +65 -0
  56. package/dist/utils/autogenerateFlow.js +391 -0
  57. package/dist/utils/autogenerateFlow.test.d.ts +1 -0
  58. package/dist/utils/autogenerateFlow.test.js +610 -0
  59. package/dist/utils/bootstrap.d.ts +1 -0
  60. package/dist/utils/bootstrap.js +46 -0
  61. package/dist/utils/commandFactory.d.ts +11 -0
  62. package/dist/utils/commandFactory.js +69 -0
  63. package/dist/utils/commitMessages.d.ts +35 -0
  64. package/dist/utils/commitMessages.js +33 -0
  65. package/dist/utils/commitMessages.test.d.ts +1 -0
  66. package/dist/utils/commitMessages.test.js +79 -0
  67. package/dist/utils/configDetection.d.ts +7 -0
  68. package/dist/utils/configDetection.js +49 -0
  69. package/dist/utils/configDetection.test.d.ts +1 -0
  70. package/dist/utils/configDetection.test.js +119 -0
  71. package/dist/utils/contentGeneration.d.ts +10 -0
  72. package/dist/utils/contentGeneration.js +141 -0
  73. package/dist/utils/contentGeneration.test.d.ts +1 -0
  74. package/dist/utils/contentGeneration.test.js +147 -0
  75. package/dist/utils/contextAnalysis.d.ts +100 -0
  76. package/dist/utils/contextAnalysis.js +308 -0
  77. package/dist/utils/contextAnalysis.test.d.ts +1 -0
  78. package/dist/utils/contextAnalysis.test.js +307 -0
  79. package/dist/utils/gitNotes.d.ts +23 -0
  80. package/dist/utils/gitNotes.js +53 -0
  81. package/dist/utils/gitNotes.test.d.ts +1 -0
  82. package/dist/utils/gitNotes.test.js +105 -0
  83. package/dist/utils/ignoreMatcher.d.ts +9 -0
  84. package/dist/utils/ignoreMatcher.js +77 -0
  85. package/dist/utils/ignoreMatcher.test.d.ts +1 -0
  86. package/dist/utils/ignoreMatcher.test.js +126 -0
  87. package/dist/utils/stateManager.d.ts +10 -0
  88. package/dist/utils/stateManager.js +30 -0
  89. package/package.json +90 -0
  90. package/scripts/convert-legacy.cjs +17 -0
  91. package/scripts/postinstall.cjs +38 -0
@@ -0,0 +1,610 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { createInitialState, generateContentForSection, validateContent, handleAccept, handleEdit, handleRegenerate, formatPresentationPrompt, formatEditPrompt, formatRegeneratePrompt, formatMaxAttemptsPrompt, parseUserChoice, isCancel, MAX_REGENERATION_ATTEMPTS, MIN_CONTEXT_SIGNALS, evaluateContextSufficiency, evaluateAmbiguity, formatFallbackPrompt, formatAutogenerationFailure, parseFallbackChoice, resolveFallbackAction, handleFallbackManual, handleFallbackAcceptPartial, handleFallbackRegenerate, } from './autogenerateFlow.js';
3
+ describe('autogenerateFlow', () => {
4
+ let mockContext;
5
+ let initialState;
6
+ beforeEach(() => {
7
+ mockContext = {
8
+ raw: {
9
+ manifests: [
10
+ {
11
+ type: 'npm',
12
+ dependencies: ['express'],
13
+ devDependencies: ['jest'],
14
+ scripts: { test: 'jest' },
15
+ },
16
+ ],
17
+ docs: [
18
+ {
19
+ filename: 'README.md',
20
+ content: 'Test project for unit testing',
21
+ },
22
+ ],
23
+ git: {
24
+ commitCount: 50,
25
+ conventionalCommits: 45,
26
+ patterns: ['feat:', 'fix:'],
27
+ warnings: [],
28
+ },
29
+ structure: {
30
+ structure: ['src/', 'src/index.ts'],
31
+ fileExtensions: ['.ts', '.js'],
32
+ warnings: [],
33
+ },
34
+ ignores: { patterns: ['node_modules/', '*.log'] },
35
+ cicd: [],
36
+ },
37
+ insights: {
38
+ techStack: {
39
+ languages: [{ name: 'JavaScript', confidence: 0.9, percentage: 100 }],
40
+ frameworks: [{ name: 'Express', version: '^4.18.0', confidence: 0.9 }],
41
+ databases: [],
42
+ infrastructure: [],
43
+ testing: [{ framework: 'Jest', confidence: 0.9 }],
44
+ },
45
+ product: {
46
+ targetUsers: ['developers'],
47
+ coreProblems: ['testing'],
48
+ keyFeatures: ['testing framework'],
49
+ confidence: 0.8,
50
+ },
51
+ workflow: {
52
+ commitConvention: 'conventional',
53
+ testingStrategy: 'TDD with Jest',
54
+ branchStrategy: 'git-flow',
55
+ confidence: 0.85,
56
+ },
57
+ projectType: 'application',
58
+ maturity: 'active',
59
+ },
60
+ meta: {
61
+ analyzedAt: new Date().toISOString(),
62
+ analysisTimeMs: 100,
63
+ filesAnalyzed: 10,
64
+ categoriesCompleted: ['manifests', 'docs', 'git'],
65
+ warnings: [],
66
+ },
67
+ };
68
+ initialState = createInitialState('2.1_product_guide');
69
+ });
70
+ describe('createInitialState', () => {
71
+ it('should create initial state with correct defaults', () => {
72
+ const state = createInitialState('test_question');
73
+ expect(state.questionId).toBe('test_question');
74
+ expect(state.attemptNumber).toBe(1);
75
+ expect(state.status).toBe('generating');
76
+ expect(state.content).toBe('');
77
+ expect(state.userChoice).toBeNull();
78
+ expect(state.guidanceHistory).toEqual([]);
79
+ expect(state.editHistory).toEqual([]);
80
+ expect(state.timestamp).toBeTruthy();
81
+ });
82
+ });
83
+ describe('generateContentForSection', () => {
84
+ it('should generate content for product_guide section', () => {
85
+ const result = generateContentForSection('product_guide', mockContext);
86
+ expect(result.success).toBe(true);
87
+ expect(result.content).toBeTruthy();
88
+ expect(result.error).toBeUndefined();
89
+ });
90
+ it('should report failure when confidence below threshold', () => {
91
+ const lowConfidenceContext = {
92
+ ...mockContext,
93
+ insights: {
94
+ ...mockContext.insights,
95
+ techStack: {
96
+ ...mockContext.insights.techStack,
97
+ frameworks: [],
98
+ testing: [],
99
+ },
100
+ workflow: {
101
+ ...mockContext.insights.workflow,
102
+ testingStrategy: 'unknown',
103
+ commitConvention: 'none',
104
+ },
105
+ },
106
+ raw: {
107
+ ...mockContext.raw,
108
+ docs: [],
109
+ cicd: [],
110
+ manifests: [],
111
+ git: {
112
+ commitCount: 0,
113
+ conventionalCommits: 0,
114
+ patterns: [],
115
+ warnings: [],
116
+ },
117
+ structure: {
118
+ structure: [],
119
+ fileExtensions: [],
120
+ warnings: [],
121
+ },
122
+ },
123
+ };
124
+ const result = generateContentForSection('workflow', lowConfidenceContext);
125
+ expect(result.success).toBe(false);
126
+ expect(result.error).toContain('Insufficient context');
127
+ expect(result.failureSummary).toContain('Autogeneration failed');
128
+ expect(result.fallbackPrompt).toContain('Continue with manual Q&A');
129
+ expect(result.fallbackState?.sufficient).toBe(false);
130
+ });
131
+ it('should generate content for product_guidelines section', () => {
132
+ const result = generateContentForSection('product_guidelines', mockContext);
133
+ expect(result.success).toBe(true);
134
+ expect(result.content).toBeTruthy();
135
+ });
136
+ it('should generate content for tech_stack section', () => {
137
+ const result = generateContentForSection('tech_stack', mockContext);
138
+ expect(result.success).toBe(true);
139
+ expect(result.content).toBeTruthy();
140
+ expect(result.content).toContain('JavaScript');
141
+ });
142
+ it('should generate content for workflow section', () => {
143
+ const result = generateContentForSection('workflow', mockContext);
144
+ expect(result.success).toBe(true);
145
+ expect(result.content).toBeTruthy();
146
+ });
147
+ it('should reject low confidence context during generation', () => {
148
+ const lowConfidenceContext = {
149
+ ...mockContext,
150
+ insights: {
151
+ ...mockContext.insights,
152
+ product: {
153
+ ...mockContext.insights.product,
154
+ confidence: 0.3,
155
+ },
156
+ workflow: {
157
+ ...mockContext.insights.workflow,
158
+ confidence: 0.3,
159
+ },
160
+ techStack: {
161
+ ...mockContext.insights.techStack,
162
+ frameworks: [],
163
+ languages: [],
164
+ },
165
+ },
166
+ raw: {
167
+ ...mockContext.raw,
168
+ docs: [],
169
+ cicd: [],
170
+ manifests: [],
171
+ git: {
172
+ commitCount: 0,
173
+ conventionalCommits: 0,
174
+ patterns: [],
175
+ warnings: [],
176
+ },
177
+ structure: {
178
+ structure: [],
179
+ fileExtensions: [],
180
+ warnings: [],
181
+ },
182
+ },
183
+ };
184
+ const result = generateContentForSection('product_guide', lowConfidenceContext);
185
+ expect(result.success).toBe(false);
186
+ expect(result.error).toContain('Insufficient context');
187
+ expect(result.fallbackPrompt).toContain('Provide more context');
188
+ expect(result.ambiguityState?.ambiguous).toBe(true);
189
+ });
190
+ });
191
+ describe('validateContent', () => {
192
+ describe('product_guide validation', () => {
193
+ it('should accept valid product guide content', () => {
194
+ const result = validateContent('This is a developer tool for managing project specifications', 'product_guide');
195
+ expect(result.valid).toBe(true);
196
+ });
197
+ it('should reject empty content', () => {
198
+ const result = validateContent('', 'product_guide');
199
+ expect(result.valid).toBe(false);
200
+ expect(result.error).toContain('cannot be empty');
201
+ });
202
+ it('should reject content shorter than 20 characters', () => {
203
+ const result = validateContent('Too short', 'product_guide');
204
+ expect(result.valid).toBe(false);
205
+ expect(result.error).toContain('at least 20 characters');
206
+ });
207
+ it('should reject incoherent content', () => {
208
+ const result = validateContent('xx yy zz aa bb cc dd ee', 'product_guide');
209
+ expect(result.valid).toBe(false);
210
+ expect(result.error).toContain('coherent text');
211
+ });
212
+ });
213
+ describe('tech_stack validation', () => {
214
+ it('should accept valid tech stack', () => {
215
+ const result = validateContent('Node.js, TypeScript, Express', 'tech_stack');
216
+ expect(result.valid).toBe(true);
217
+ });
218
+ it('should reject very short tech stack', () => {
219
+ const result = validateContent('JS', 'tech_stack');
220
+ expect(result.valid).toBe(false);
221
+ });
222
+ });
223
+ describe('workflow validation', () => {
224
+ it('should accept valid workflow', () => {
225
+ const result = validateContent('We use TDD with Jest and conventional commits', 'workflow');
226
+ expect(result.valid).toBe(true);
227
+ });
228
+ it('should reject workflow shorter than 30 characters', () => {
229
+ const result = validateContent('We use TDD', 'workflow');
230
+ expect(result.valid).toBe(false);
231
+ expect(result.error).toContain('at least 30 characters');
232
+ });
233
+ });
234
+ });
235
+ describe('handleAccept', () => {
236
+ it('should accept valid content', () => {
237
+ const state = {
238
+ ...initialState,
239
+ content: 'A valid product guide description with sufficient length',
240
+ };
241
+ const result = handleAccept(state, 'product_guide');
242
+ expect(result.success).toBe(true);
243
+ expect(result.content).toBe(state.content);
244
+ expect(result.state?.status).toBe('accepted');
245
+ expect(result.state?.userChoice).toBe('A');
246
+ });
247
+ it('should reject invalid content', () => {
248
+ const state = {
249
+ ...initialState,
250
+ content: 'Too short',
251
+ };
252
+ const result = handleAccept(state, 'product_guide');
253
+ expect(result.success).toBe(false);
254
+ expect(result.error).toBeTruthy();
255
+ });
256
+ });
257
+ describe('handleEdit', () => {
258
+ it('should accept valid edited content', () => {
259
+ const editedContent = 'This is the user edited version with enough length';
260
+ const result = handleEdit(initialState, editedContent, 'product_guide');
261
+ expect(result.success).toBe(true);
262
+ expect(result.content).toBe(editedContent);
263
+ expect(result.state?.status).toBe('edited');
264
+ expect(result.state?.userChoice).toBe('B');
265
+ expect(result.state?.editHistory).toContain(editedContent);
266
+ });
267
+ it('should reject invalid edited content', () => {
268
+ const result = handleEdit(initialState, 'Short', 'product_guide');
269
+ expect(result.success).toBe(false);
270
+ expect(result.error).toBeTruthy();
271
+ });
272
+ it('should track edit history', () => {
273
+ const edit1 = 'First edit with sufficient length for validation';
274
+ const edit2 = 'Second edit also with sufficient length for validation';
275
+ const result1 = handleEdit(initialState, edit1, 'product_guide');
276
+ expect(result1.success).toBe(true);
277
+ const result2 = handleEdit(result1.state, edit2, 'product_guide');
278
+ expect(result2.success).toBe(true);
279
+ expect(result2.state?.editHistory).toEqual([edit1, edit2]);
280
+ });
281
+ });
282
+ describe('handleRegenerate', () => {
283
+ it('should regenerate content with valid guidance', () => {
284
+ const state = {
285
+ ...initialState,
286
+ content: 'Original content',
287
+ };
288
+ const result = handleRegenerate(state, 'Focus more on developers', 'product_guide', mockContext);
289
+ expect(result.success).toBe(true);
290
+ expect(result.content).toBeTruthy();
291
+ expect(result.state?.status).toBe('presented');
292
+ expect(result.state?.userChoice).toBe('C');
293
+ expect(result.state?.attemptNumber).toBe(2);
294
+ expect(result.state?.guidanceHistory).toContain('Focus more on developers');
295
+ });
296
+ it('should reject guidance that is too short', () => {
297
+ const result = handleRegenerate(initialState, 'ok', 'product_guide', mockContext);
298
+ expect(result.success).toBe(false);
299
+ expect(result.error).toContain('at least 5 characters');
300
+ });
301
+ it('should enforce maximum regeneration attempts', () => {
302
+ let state = initialState;
303
+ state.attemptNumber = MAX_REGENERATION_ATTEMPTS;
304
+ const result = handleRegenerate(state, 'Some valid guidance', 'product_guide', mockContext);
305
+ expect(result.success).toBe(false);
306
+ expect(result.error).toContain('Maximum regeneration attempts');
307
+ });
308
+ it('should track guidance history', () => {
309
+ let state = initialState;
310
+ const guidance1 = 'Focus on developers';
311
+ const result1 = handleRegenerate(state, guidance1, 'product_guide', mockContext);
312
+ expect(result1.success).toBe(true);
313
+ state = result1.state;
314
+ const guidance2 = 'Be more specific';
315
+ const result2 = handleRegenerate(state, guidance2, 'product_guide', mockContext);
316
+ expect(result2.success).toBe(true);
317
+ expect(result2.state?.guidanceHistory).toEqual([guidance1, guidance2]);
318
+ });
319
+ });
320
+ describe('formatPresentationPrompt', () => {
321
+ it('should format presentation prompt correctly', () => {
322
+ const content = 'Test content';
323
+ const prompt = formatPresentationPrompt(content);
324
+ expect(prompt).toContain('autogenerated the following content');
325
+ expect(prompt).toContain(content);
326
+ expect(prompt).toContain('A) Accept');
327
+ expect(prompt).toContain('B) Edit');
328
+ expect(prompt).toContain('C) Regenerate');
329
+ });
330
+ });
331
+ describe('formatEditPrompt', () => {
332
+ it('should format edit prompt correctly', () => {
333
+ const content = 'Original content';
334
+ const prompt = formatEditPrompt(content);
335
+ expect(prompt).toContain('You selected Edit');
336
+ expect(prompt).toContain(content);
337
+ expect(prompt).toContain('cancel');
338
+ });
339
+ });
340
+ describe('formatRegeneratePrompt', () => {
341
+ it('should format regenerate prompt correctly', () => {
342
+ const prompt = formatRegeneratePrompt();
343
+ expect(prompt).toContain('You selected Regenerate');
344
+ expect(prompt).toContain('provide guidance');
345
+ expect(prompt).toContain('cancel');
346
+ });
347
+ });
348
+ describe('formatMaxAttemptsPrompt', () => {
349
+ it('should format max attempts prompt correctly', () => {
350
+ const prompt = formatMaxAttemptsPrompt();
351
+ expect(prompt).toContain('regenerated content 3 times');
352
+ expect(prompt).toContain('A) Accept');
353
+ expect(prompt).toContain('B) Edit');
354
+ expect(prompt).toContain('C) Switch to manual Q&A');
355
+ });
356
+ });
357
+ describe('evaluateContextSufficiency', () => {
358
+ it('should report sufficient context with multiple signals', () => {
359
+ const result = evaluateContextSufficiency(mockContext);
360
+ expect(result.sufficient).toBe(true);
361
+ expect(result.reason).toContain('context sources');
362
+ expect(result.missing.length).toBeLessThan(6);
363
+ });
364
+ it('should report insufficient context with minimal signals', () => {
365
+ const minimalContext = {
366
+ ...mockContext,
367
+ raw: {
368
+ ...mockContext.raw,
369
+ manifests: [],
370
+ docs: [],
371
+ git: { ...mockContext.raw.git, commitCount: 0 },
372
+ structure: { ...mockContext.raw.structure, structure: [] },
373
+ ignores: { patterns: [] },
374
+ cicd: [],
375
+ },
376
+ meta: {
377
+ ...mockContext.meta,
378
+ warnings: [
379
+ 'No manifest files found',
380
+ 'No README found',
381
+ 'Not a git repository',
382
+ ],
383
+ },
384
+ };
385
+ const result = evaluateContextSufficiency(minimalContext);
386
+ expect(result.sufficient).toBe(false);
387
+ expect(result.reason).toContain('Not enough project context');
388
+ expect(result.missing.length).toBeGreaterThanOrEqual(MIN_CONTEXT_SIGNALS);
389
+ });
390
+ });
391
+ describe('evaluateAmbiguity', () => {
392
+ it('should detect ambiguous contexts', () => {
393
+ const ambiguousContext = {
394
+ ...mockContext,
395
+ insights: {
396
+ ...mockContext.insights,
397
+ projectType: 'unknown',
398
+ techStack: { ...mockContext.insights.techStack, languages: [] },
399
+ },
400
+ meta: {
401
+ ...mockContext.meta,
402
+ filesAnalyzed: 0,
403
+ warnings: ['No manifest files found', 'No README found'],
404
+ },
405
+ };
406
+ const result = evaluateAmbiguity(ambiguousContext);
407
+ expect(result.ambiguous).toBe(true);
408
+ expect(result.signals.length).toBeGreaterThan(0);
409
+ });
410
+ });
411
+ describe('formatFallbackPrompt', () => {
412
+ it('should include reason and missing data', () => {
413
+ const prompt = formatFallbackPrompt('Insufficient context', ['manifests']);
414
+ expect(prompt).toContain('Insufficient context');
415
+ expect(prompt).toContain('Missing: manifests');
416
+ expect(prompt).toContain('A) Continue with manual Q&A');
417
+ });
418
+ });
419
+ describe('formatAutogenerationFailure', () => {
420
+ it('should format a detailed failure summary', () => {
421
+ const sufficiency = {
422
+ sufficient: false,
423
+ reason: 'Not enough project context sources',
424
+ missing: ['docs'],
425
+ };
426
+ const ambiguity = {
427
+ ambiguous: true,
428
+ reason: 'Context ambiguous',
429
+ signals: ['Missing README'],
430
+ };
431
+ const message = formatAutogenerationFailure('product_guide', 'Insufficient context', sufficiency, ambiguity);
432
+ expect(message).toContain('Autogeneration failed');
433
+ expect(message).toContain('Insufficient context');
434
+ expect(message).toContain('Missing: docs');
435
+ expect(message).toContain('Ambiguity signals');
436
+ });
437
+ });
438
+ describe('parseFallbackChoice', () => {
439
+ it('should parse manual choice', () => {
440
+ expect(parseFallbackChoice('A')).toBe('A');
441
+ expect(parseFallbackChoice('manual')).toBe('A');
442
+ expect(parseFallbackChoice('m')).toBe('A');
443
+ });
444
+ it('should parse regenerate choice', () => {
445
+ expect(parseFallbackChoice('B')).toBe('B');
446
+ expect(parseFallbackChoice('regenerate')).toBe('B');
447
+ expect(parseFallbackChoice('context')).toBe('B');
448
+ });
449
+ it('should parse accept partial choice', () => {
450
+ expect(parseFallbackChoice('C')).toBe('C');
451
+ expect(parseFallbackChoice('partial')).toBe('C');
452
+ expect(parseFallbackChoice('accept')).toBe('C');
453
+ });
454
+ it('should reject invalid fallback choices', () => {
455
+ expect(parseFallbackChoice('D')).toBeNull();
456
+ expect(parseFallbackChoice('')).toBeNull();
457
+ });
458
+ });
459
+ describe('resolveFallbackAction', () => {
460
+ it('should resolve manual action', () => {
461
+ expect(resolveFallbackAction('A')).toBe('manual');
462
+ expect(resolveFallbackAction(null)).toBe('manual');
463
+ });
464
+ it('should resolve regenerate action', () => {
465
+ expect(resolveFallbackAction('B')).toBe('regenerate');
466
+ });
467
+ it('should resolve accept partial action', () => {
468
+ expect(resolveFallbackAction('C')).toBe('accept_partial');
469
+ });
470
+ });
471
+ describe('parseUserChoice', () => {
472
+ it('should parse Accept choices', () => {
473
+ expect(parseUserChoice('A')).toBe('A');
474
+ expect(parseUserChoice('a')).toBe('A');
475
+ expect(parseUserChoice('Accept')).toBe('A');
476
+ expect(parseUserChoice('accept')).toBe('A');
477
+ expect(parseUserChoice('yes')).toBe('A');
478
+ expect(parseUserChoice('y')).toBe('A');
479
+ expect(parseUserChoice(' A ')).toBe('A');
480
+ });
481
+ it('should parse Edit choices', () => {
482
+ expect(parseUserChoice('B')).toBe('B');
483
+ expect(parseUserChoice('b')).toBe('B');
484
+ expect(parseUserChoice('Edit')).toBe('B');
485
+ expect(parseUserChoice('edit')).toBe('B');
486
+ expect(parseUserChoice('e')).toBe('B');
487
+ });
488
+ it('should parse Regenerate choices', () => {
489
+ expect(parseUserChoice('C')).toBe('C');
490
+ expect(parseUserChoice('c')).toBe('C');
491
+ expect(parseUserChoice('Regenerate')).toBe('C');
492
+ expect(parseUserChoice('regenerate')).toBe('C');
493
+ expect(parseUserChoice('r')).toBe('C');
494
+ });
495
+ it('should return null for invalid choices', () => {
496
+ expect(parseUserChoice('D')).toBeNull();
497
+ expect(parseUserChoice('invalid')).toBeNull();
498
+ expect(parseUserChoice('123')).toBeNull();
499
+ expect(parseUserChoice('')).toBeNull();
500
+ });
501
+ });
502
+ describe('isCancel', () => {
503
+ it('should detect cancel command', () => {
504
+ expect(isCancel('cancel')).toBe(true);
505
+ expect(isCancel('Cancel')).toBe(true);
506
+ expect(isCancel('CANCEL')).toBe(true);
507
+ expect(isCancel(' cancel ')).toBe(true);
508
+ });
509
+ it('should return false for non-cancel input', () => {
510
+ expect(isCancel('A')).toBe(false);
511
+ expect(isCancel('no')).toBe(false);
512
+ expect(isCancel('back')).toBe(false);
513
+ expect(isCancel('')).toBe(false);
514
+ });
515
+ });
516
+ describe('Fallback Handlers', () => {
517
+ describe('handleFallbackManual', () => {
518
+ it('should return failure with manual fallback action', () => {
519
+ const result = handleFallbackManual('product_guide', 'Insufficient context');
520
+ expect(result.success).toBe(false);
521
+ expect(result.error).toContain('Fallback to manual Q&A');
522
+ expect(result.error).toContain('Insufficient context');
523
+ expect(result.fallbackAction).toBe('manual');
524
+ });
525
+ });
526
+ describe('handleFallbackAcceptPartial', () => {
527
+ it('should accept valid partial content', () => {
528
+ const partialContent = 'This is a partial product description with limited details';
529
+ const result = handleFallbackAcceptPartial(partialContent, 'product_guide', 'Low confidence generation');
530
+ expect(result.success).toBe(true);
531
+ expect(result.content).toBe(partialContent);
532
+ expect(result.fallbackAction).toBe('accept_partial');
533
+ });
534
+ it('should reject invalid partial content and fall back to manual', () => {
535
+ const invalidContent = 'xyz';
536
+ const result = handleFallbackAcceptPartial(invalidContent, 'product_guide', 'Low confidence');
537
+ expect(result.success).toBe(false);
538
+ expect(result.error).toContain('Partial content is invalid');
539
+ expect(result.fallbackAction).toBe('manual');
540
+ });
541
+ it('should reject empty partial content', () => {
542
+ const result = handleFallbackAcceptPartial('', 'product_guide', 'Empty generation');
543
+ expect(result.success).toBe(false);
544
+ expect(result.error).toContain('Partial content is invalid');
545
+ expect(result.fallbackAction).toBe('manual');
546
+ });
547
+ });
548
+ describe('handleFallbackRegenerate', () => {
549
+ it('should regenerate content with updated context', () => {
550
+ const result = handleFallbackRegenerate('product_guide', mockContext, 'Focus on developer tooling aspect');
551
+ expect(result.success).toBe(true);
552
+ expect(result.content).toBeTruthy();
553
+ });
554
+ it('should fail regeneration with insufficient context', () => {
555
+ const minimalContext = {
556
+ raw: {
557
+ manifests: [],
558
+ docs: [],
559
+ cicd: [],
560
+ ignores: { patterns: [] },
561
+ git: {
562
+ commitCount: 0,
563
+ conventionalCommits: 0,
564
+ patterns: [],
565
+ warnings: [],
566
+ },
567
+ structure: {
568
+ structure: [],
569
+ fileExtensions: [],
570
+ warnings: [],
571
+ },
572
+ },
573
+ insights: {
574
+ techStack: {
575
+ languages: [],
576
+ frameworks: [],
577
+ databases: [],
578
+ infrastructure: [],
579
+ testing: [],
580
+ },
581
+ product: {
582
+ targetUsers: [],
583
+ coreProblems: [],
584
+ keyFeatures: [],
585
+ confidence: 0.0,
586
+ },
587
+ workflow: {
588
+ commitConvention: 'none',
589
+ testingStrategy: 'unknown',
590
+ branchStrategy: 'unknown',
591
+ confidence: 0.0,
592
+ },
593
+ projectType: 'unknown',
594
+ maturity: 'early',
595
+ },
596
+ meta: {
597
+ analyzedAt: new Date().toISOString(),
598
+ analysisTimeMs: 0,
599
+ filesAnalyzed: 0,
600
+ categoriesCompleted: [],
601
+ warnings: [],
602
+ },
603
+ };
604
+ const result = handleFallbackRegenerate('workflow', minimalContext, 'Please add more workflow details');
605
+ expect(result.success).toBe(false);
606
+ expect(result.error).toContain('Insufficient context');
607
+ });
608
+ });
609
+ });
610
+ });
@@ -0,0 +1 @@
1
+ export declare function bootstrap(ctx: any): Promise<void>;
@@ -0,0 +1,46 @@
1
+ import { existsSync, mkdirSync, copyFileSync, readdirSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { homedir } from "os";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ export async function bootstrap(ctx) {
8
+ const opencodeConfigDir = join(homedir(), ".config", "opencode");
9
+ const targetAgentDir = join(opencodeConfigDir, "agent");
10
+ const targetCommandDir = join(opencodeConfigDir, "command");
11
+ const sourcePromptsDir = join(__dirname, "../prompts");
12
+ const sourceAgentFile = join(sourcePromptsDir, "agent/cdd.md");
13
+ const sourceCommandsDir = join(sourcePromptsDir, "commands");
14
+ let installedAnything = false;
15
+ // 1. Ensure directories exist
16
+ if (!existsSync(targetAgentDir))
17
+ mkdirSync(targetAgentDir, { recursive: true });
18
+ if (!existsSync(targetCommandDir))
19
+ mkdirSync(targetCommandDir, { recursive: true });
20
+ // 2. Install/Update Agent
21
+ const targetAgentFile = join(targetAgentDir, "cdd.md");
22
+ if (existsSync(sourceAgentFile)) {
23
+ copyFileSync(sourceAgentFile, targetAgentFile);
24
+ installedAnything = true;
25
+ }
26
+ // 3. Install/Update Commands
27
+ if (existsSync(sourceCommandsDir)) {
28
+ const commands = readdirSync(sourceCommandsDir);
29
+ for (const cmdFile of commands) {
30
+ const targetCmdFile = join(targetCommandDir, cmdFile);
31
+ copyFileSync(join(sourceCommandsDir, cmdFile), targetCmdFile);
32
+ installedAnything = true;
33
+ }
34
+ }
35
+ if (installedAnything) {
36
+ // Do not await toasts during bootstrapping as the TUI might not be ready
37
+ ctx.client.tui.showToast({
38
+ body: {
39
+ title: "Conductor CDD",
40
+ message: "First-run setup: CDD agent and commands installed globally. Please restart OpenCode to enable slash commands.",
41
+ variant: "info",
42
+ duration: 5000
43
+ }
44
+ }).catch(() => { });
45
+ }
46
+ }
@@ -0,0 +1,11 @@
1
+ import { type PluginInput } from "@opencode-ai/plugin";
2
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
3
+ interface CDDCommandConfig {
4
+ name: string;
5
+ description: string;
6
+ args: Record<string, any>;
7
+ requiresSetup?: boolean;
8
+ additionalContext?: (ctx: PluginInput, args: any) => Promise<Record<string, string>>;
9
+ }
10
+ export declare function createCDDCommand(config: CDDCommandConfig): (ctx: PluginInput) => ToolDefinition;
11
+ export {};