opencode-conductor-cdd-plugin 1.0.0-beta.18 → 1.0.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/dist/prompts/cdd/setup.json +2 -2
- package/dist/prompts/cdd/setup.test.js +40 -118
- package/dist/prompts/cdd/setup.test.ts +40 -143
- package/dist/utils/codebaseAnalysis.d.ts +61 -0
- package/dist/utils/codebaseAnalysis.js +429 -0
- package/dist/utils/codebaseAnalysis.test.d.ts +1 -0
- package/dist/utils/codebaseAnalysis.test.js +556 -0
- package/dist/utils/configDetection.d.ts +12 -0
- package/dist/utils/configDetection.js +23 -9
- package/dist/utils/configDetection.test.js +204 -7
- package/dist/utils/documentGeneration.d.ts +97 -0
- package/dist/utils/documentGeneration.js +301 -0
- package/dist/utils/documentGeneration.test.d.ts +1 -0
- package/dist/utils/documentGeneration.test.js +380 -0
- package/dist/utils/interactiveMenu.d.ts +56 -0
- package/dist/utils/interactiveMenu.js +144 -0
- package/dist/utils/interactiveMenu.test.d.ts +1 -0
- package/dist/utils/interactiveMenu.test.js +231 -0
- package/dist/utils/interactiveSetup.d.ts +43 -0
- package/dist/utils/interactiveSetup.js +131 -0
- package/dist/utils/interactiveSetup.test.d.ts +1 -0
- package/dist/utils/interactiveSetup.test.js +124 -0
- package/dist/utils/projectMaturity.d.ts +53 -0
- package/dist/utils/projectMaturity.js +179 -0
- package/dist/utils/projectMaturity.test.d.ts +1 -0
- package/dist/utils/projectMaturity.test.js +298 -0
- package/dist/utils/questionGenerator.d.ts +51 -0
- package/dist/utils/questionGenerator.js +535 -0
- package/dist/utils/questionGenerator.test.d.ts +1 -0
- package/dist/utils/questionGenerator.test.js +328 -0
- package/dist/utils/setupIntegration.d.ts +72 -0
- package/dist/utils/setupIntegration.js +179 -0
- package/dist/utils/setupIntegration.test.d.ts +1 -0
- package/dist/utils/setupIntegration.test.js +344 -0
- package/dist/utils/synergyState.test.js +17 -3
- package/package.json +2 -1
|
@@ -119,7 +119,7 @@ describe("configDetection", () => {
|
|
|
119
119
|
});
|
|
120
120
|
// New tests for oh-my-opencode-slim detection
|
|
121
121
|
describe("oh-my-opencode-slim detection", () => {
|
|
122
|
-
it("should detect
|
|
122
|
+
it("should NOT detect synergy when slim config has no cdd agent", () => {
|
|
123
123
|
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
124
124
|
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
125
125
|
if (path === slimJsonPath) {
|
|
@@ -130,11 +130,11 @@ describe("configDetection", () => {
|
|
|
130
130
|
return "";
|
|
131
131
|
});
|
|
132
132
|
const result = detectCDDConfig();
|
|
133
|
-
expect(result.synergyActive).toBe(
|
|
134
|
-
expect(result.synergyFramework).toBe('
|
|
133
|
+
expect(result.synergyActive).toBe(false);
|
|
134
|
+
expect(result.synergyFramework).toBe('none');
|
|
135
135
|
expect(result.slimAgents).toEqual(['explorer', 'librarian', 'oracle', 'designer']);
|
|
136
136
|
});
|
|
137
|
-
it("should prioritize oh-my-opencode
|
|
137
|
+
it("should prioritize oh-my-opencode over slim when slim has no cdd agent", () => {
|
|
138
138
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
139
139
|
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
140
140
|
if (path === slimJsonPath) {
|
|
@@ -146,10 +146,11 @@ describe("configDetection", () => {
|
|
|
146
146
|
return "";
|
|
147
147
|
});
|
|
148
148
|
const result = detectCDDConfig();
|
|
149
|
-
expect(result.synergyFramework).toBe('oh-my-opencode
|
|
149
|
+
expect(result.synergyFramework).toBe('oh-my-opencode');
|
|
150
150
|
expect(result.synergyActive).toBe(true);
|
|
151
|
+
expect(result.cddModel).toBe('model-from-omo');
|
|
151
152
|
});
|
|
152
|
-
it("should filter out disabled agents from slim config", () => {
|
|
153
|
+
it("should filter out disabled agents from slim config (but require cdd for synergy)", () => {
|
|
153
154
|
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
154
155
|
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
155
156
|
if (path === slimJsonPath) {
|
|
@@ -160,7 +161,8 @@ describe("configDetection", () => {
|
|
|
160
161
|
return "";
|
|
161
162
|
});
|
|
162
163
|
const result = detectCDDConfig();
|
|
163
|
-
expect(result.synergyFramework).toBe('
|
|
164
|
+
expect(result.synergyFramework).toBe('none');
|
|
165
|
+
expect(result.synergyActive).toBe(false);
|
|
164
166
|
expect(result.slimAgents).toEqual(['explorer', 'librarian']);
|
|
165
167
|
});
|
|
166
168
|
it("should handle empty oh-my-opencode-slim config", () => {
|
|
@@ -203,4 +205,199 @@ describe("configDetection", () => {
|
|
|
203
205
|
expect(result.synergyActive).toBe(false);
|
|
204
206
|
});
|
|
205
207
|
});
|
|
208
|
+
// New tests for CDD agent detection in oh-my-opencode-slim
|
|
209
|
+
describe("CDD agent detection in oh-my-opencode-slim", () => {
|
|
210
|
+
it("should detect cdd agent in slim config and activate synergy", () => {
|
|
211
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
212
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
213
|
+
if (path === slimJsonPath) {
|
|
214
|
+
return JSON.stringify({
|
|
215
|
+
agents: {
|
|
216
|
+
cdd: { model: "anthropic/claude-3-5-sonnet" },
|
|
217
|
+
designer: { model: "google/gemini-3-flash" }
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return "";
|
|
222
|
+
});
|
|
223
|
+
const result = detectCDDConfig();
|
|
224
|
+
expect(result.hasCDDInSlim).toBe(true);
|
|
225
|
+
expect(result.synergyFramework).toBe('oh-my-opencode-slim');
|
|
226
|
+
expect(result.synergyActive).toBe(true);
|
|
227
|
+
expect(result.cddModel).toBe("anthropic/claude-3-5-sonnet");
|
|
228
|
+
});
|
|
229
|
+
it("should not activate synergy when slim config exists but no cdd agent", () => {
|
|
230
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
231
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
232
|
+
if (path === slimJsonPath) {
|
|
233
|
+
return JSON.stringify({
|
|
234
|
+
agents: {
|
|
235
|
+
designer: { model: "google/gemini-3-flash" }
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return "";
|
|
240
|
+
});
|
|
241
|
+
const result = detectCDDConfig();
|
|
242
|
+
expect(result.hasCDDInSlim).toBe(false);
|
|
243
|
+
expect(result.synergyFramework).toBe('none');
|
|
244
|
+
expect(result.synergyActive).toBe(false);
|
|
245
|
+
});
|
|
246
|
+
it("should not activate synergy when slim config has empty agents object", () => {
|
|
247
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
248
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
249
|
+
if (path === slimJsonPath) {
|
|
250
|
+
return JSON.stringify({ agents: {} });
|
|
251
|
+
}
|
|
252
|
+
return "";
|
|
253
|
+
});
|
|
254
|
+
const result = detectCDDConfig();
|
|
255
|
+
expect(result.hasCDDInSlim).toBe(false);
|
|
256
|
+
expect(result.synergyFramework).toBe('none');
|
|
257
|
+
expect(result.synergyActive).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
it("should handle malformed slim config gracefully with hasCDDInSlim false", () => {
|
|
260
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
261
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
262
|
+
if (path === slimJsonPath) {
|
|
263
|
+
return "invalid json";
|
|
264
|
+
}
|
|
265
|
+
return "";
|
|
266
|
+
});
|
|
267
|
+
const result = detectCDDConfig();
|
|
268
|
+
expect(result.hasCDDInSlim).toBe(false);
|
|
269
|
+
expect(result.synergyFramework).toBe('none');
|
|
270
|
+
expect(result.synergyActive).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
it("should extract cdd model from slim config when present", () => {
|
|
273
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
274
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
275
|
+
if (path === slimJsonPath) {
|
|
276
|
+
return JSON.stringify({
|
|
277
|
+
agents: {
|
|
278
|
+
cdd: { model: "anthropic/claude-3-5-haiku" }
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return "";
|
|
283
|
+
});
|
|
284
|
+
const result = detectCDDConfig();
|
|
285
|
+
expect(result.cddModel).toBe("anthropic/claude-3-5-haiku");
|
|
286
|
+
expect(result.hasCDDInSlim).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
it("should handle cdd agent without model field in slim", () => {
|
|
289
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
290
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
291
|
+
if (path === slimJsonPath) {
|
|
292
|
+
return JSON.stringify({
|
|
293
|
+
agents: {
|
|
294
|
+
cdd: {}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return "";
|
|
299
|
+
});
|
|
300
|
+
const result = detectCDDConfig();
|
|
301
|
+
expect(result.hasCDDInSlim).toBe(true);
|
|
302
|
+
expect(result.cddModel).toBeUndefined();
|
|
303
|
+
});
|
|
304
|
+
it("should prioritize slim model over oh-my-opencode model when both have cdd", () => {
|
|
305
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
306
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
307
|
+
if (path === slimJsonPath) {
|
|
308
|
+
return JSON.stringify({
|
|
309
|
+
agents: {
|
|
310
|
+
cdd: { model: "model-from-slim" }
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (path === omoJsonPath) {
|
|
315
|
+
return JSON.stringify({
|
|
316
|
+
agents: {
|
|
317
|
+
cdd: { model: "model-from-omo" }
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return "";
|
|
322
|
+
});
|
|
323
|
+
const result = detectCDDConfig();
|
|
324
|
+
expect(result.cddModel).toBe("model-from-slim");
|
|
325
|
+
expect(result.synergyFramework).toBe('oh-my-opencode-slim');
|
|
326
|
+
expect(result.hasCDDInSlim).toBe(true);
|
|
327
|
+
expect(result.hasCDDInOMO).toBe(true);
|
|
328
|
+
});
|
|
329
|
+
it("should select oh-my-opencode when only OMO has cdd", () => {
|
|
330
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
331
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
332
|
+
if (path === slimJsonPath) {
|
|
333
|
+
return JSON.stringify({
|
|
334
|
+
agents: {
|
|
335
|
+
designer: { model: "google/gemini-3-flash" }
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (path === omoJsonPath) {
|
|
340
|
+
return JSON.stringify({
|
|
341
|
+
agents: {
|
|
342
|
+
cdd: { model: "model-from-omo" }
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return "";
|
|
347
|
+
});
|
|
348
|
+
const result = detectCDDConfig();
|
|
349
|
+
expect(result.hasCDDInSlim).toBe(false);
|
|
350
|
+
expect(result.hasCDDInOMO).toBe(true);
|
|
351
|
+
expect(result.synergyFramework).toBe('oh-my-opencode');
|
|
352
|
+
});
|
|
353
|
+
it("should select slim when only slim has cdd", () => {
|
|
354
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
355
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
356
|
+
if (path === slimJsonPath) {
|
|
357
|
+
return JSON.stringify({
|
|
358
|
+
agents: {
|
|
359
|
+
cdd: { model: "model-from-slim" }
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
if (path === omoJsonPath) {
|
|
364
|
+
return JSON.stringify({
|
|
365
|
+
agents: {
|
|
366
|
+
sisyphus: { model: "google/gemini-3-flash" }
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return "";
|
|
371
|
+
});
|
|
372
|
+
const result = detectCDDConfig();
|
|
373
|
+
expect(result.hasCDDInSlim).toBe(true);
|
|
374
|
+
expect(result.hasCDDInOMO).toBe(false);
|
|
375
|
+
expect(result.synergyFramework).toBe('oh-my-opencode-slim');
|
|
376
|
+
});
|
|
377
|
+
it("should not activate synergy when neither framework has cdd", () => {
|
|
378
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
379
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
380
|
+
if (path === slimJsonPath) {
|
|
381
|
+
return JSON.stringify({
|
|
382
|
+
agents: {
|
|
383
|
+
designer: { model: "google/gemini-3-flash" }
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (path === omoJsonPath) {
|
|
388
|
+
return JSON.stringify({
|
|
389
|
+
agents: {
|
|
390
|
+
sisyphus: { model: "google/gemini-3-flash" }
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return "";
|
|
395
|
+
});
|
|
396
|
+
const result = detectCDDConfig();
|
|
397
|
+
expect(result.hasCDDInSlim).toBe(false);
|
|
398
|
+
expect(result.hasCDDInOMO).toBe(false);
|
|
399
|
+
expect(result.synergyFramework).toBe('none');
|
|
400
|
+
expect(result.synergyActive).toBe(false);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
206
403
|
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Question, Section } from './questionGenerator.js';
|
|
2
|
+
import { CodebaseAnalysis } from './codebaseAnalysis.js';
|
|
3
|
+
/**
|
|
4
|
+
* Document Generation Module
|
|
5
|
+
*
|
|
6
|
+
* Orchestrates the full document generation workflow:
|
|
7
|
+
* - Present questions sequentially (max 5)
|
|
8
|
+
* - Handle user selections (A-E, including custom and auto-generate)
|
|
9
|
+
* - Draft documents from selected answers only
|
|
10
|
+
* - Approval/revision loops
|
|
11
|
+
* - State persistence for resume functionality
|
|
12
|
+
*
|
|
13
|
+
* Based on reference implementations:
|
|
14
|
+
* - derekbar90/opencode-conductor
|
|
15
|
+
* - gemini-cli-extensions/conductor
|
|
16
|
+
*/
|
|
17
|
+
export interface QuestionAnswer {
|
|
18
|
+
questionId: string;
|
|
19
|
+
selections: string[];
|
|
20
|
+
selectedOptions: string[];
|
|
21
|
+
customText?: string;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
}
|
|
24
|
+
export interface QuestionSession {
|
|
25
|
+
section: Section;
|
|
26
|
+
questionsAsked: Question[];
|
|
27
|
+
answers: QuestionAnswer[];
|
|
28
|
+
autoGenerateRequested: boolean;
|
|
29
|
+
autoGenerateAtQuestion?: number;
|
|
30
|
+
startTime: string;
|
|
31
|
+
endTime: string;
|
|
32
|
+
}
|
|
33
|
+
export interface DocumentDraft {
|
|
34
|
+
content: string;
|
|
35
|
+
inferredFromContext: boolean;
|
|
36
|
+
sectionsIncluded: string[];
|
|
37
|
+
wordCount: number;
|
|
38
|
+
}
|
|
39
|
+
export interface DocumentState {
|
|
40
|
+
section: Section;
|
|
41
|
+
checkpoint: string;
|
|
42
|
+
content: string;
|
|
43
|
+
filePath: string;
|
|
44
|
+
timestamp?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface SaveStateResult {
|
|
47
|
+
success: boolean;
|
|
48
|
+
stateFile?: string;
|
|
49
|
+
checkpoint?: string;
|
|
50
|
+
filePath?: string;
|
|
51
|
+
previousCheckpoints?: string[];
|
|
52
|
+
error?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ApprovalResult {
|
|
55
|
+
approved: boolean;
|
|
56
|
+
finalContent?: string;
|
|
57
|
+
revisionGuidance?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface DocumentGenerationOptions {
|
|
60
|
+
section: Section;
|
|
61
|
+
questions: Question[];
|
|
62
|
+
analysis: CodebaseAnalysis;
|
|
63
|
+
responder: (question: Question, questionNumber: number) => Promise<string[]>;
|
|
64
|
+
approvalFlow: (draft: DocumentDraft) => Promise<ApprovalResult>;
|
|
65
|
+
outputPath: string;
|
|
66
|
+
maxRevisions?: number;
|
|
67
|
+
customInputPrompt?: (question: Question) => Promise<string>;
|
|
68
|
+
}
|
|
69
|
+
export interface DocumentGenerationResult {
|
|
70
|
+
success: boolean;
|
|
71
|
+
checkpoint?: string;
|
|
72
|
+
autoGenerated?: boolean;
|
|
73
|
+
revisionCount?: number;
|
|
74
|
+
error?: string;
|
|
75
|
+
}
|
|
76
|
+
export interface PresentQuestionsOptions {
|
|
77
|
+
maxQuestions?: number;
|
|
78
|
+
customInputPrompt?: (question: Question) => Promise<string>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Present questions sequentially to the user
|
|
82
|
+
* Stops after maxQuestions or when option E is selected
|
|
83
|
+
*/
|
|
84
|
+
export declare function presentQuestionsSequentially(questions: Question[], responder: (question: Question, questionNumber: number) => Promise<string[]>, options?: PresentQuestionsOptions): Promise<QuestionSession>;
|
|
85
|
+
/**
|
|
86
|
+
* Draft document content from user's selected answers
|
|
87
|
+
* CRITICAL: Only use selected options, ignore questions and unselected options
|
|
88
|
+
*/
|
|
89
|
+
export declare function draftDocumentFromAnswers(session: QuestionSession, analysis: CodebaseAnalysis): DocumentDraft;
|
|
90
|
+
/**
|
|
91
|
+
* Save document to file and update state
|
|
92
|
+
*/
|
|
93
|
+
export declare function saveDocumentState(state: DocumentState): Promise<SaveStateResult>;
|
|
94
|
+
/**
|
|
95
|
+
* Orchestrate full document generation workflow
|
|
96
|
+
*/
|
|
97
|
+
export declare function generateDocument(options: DocumentGenerationOptions): Promise<DocumentGenerationResult>;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { validateSelection, parseSelection } from './interactiveMenu.js';
|
|
4
|
+
// Constants
|
|
5
|
+
const MAX_QUESTIONS = 5;
|
|
6
|
+
const MAX_REVISION_ATTEMPTS = 3;
|
|
7
|
+
const STATE_FILE_PATH = 'conductor-cdd/setup_state.json';
|
|
8
|
+
const CHECKPOINT_MAP = {
|
|
9
|
+
product: '2.1_product_guide',
|
|
10
|
+
guidelines: '2.2_product_guidelines',
|
|
11
|
+
'tech-stack': '2.3_tech_stack',
|
|
12
|
+
styleguides: '2.4_code_styleguides',
|
|
13
|
+
workflow: '2.5_workflow',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Present questions sequentially to the user
|
|
17
|
+
* Stops after maxQuestions or when option E is selected
|
|
18
|
+
*/
|
|
19
|
+
export async function presentQuestionsSequentially(questions, responder, options = {}) {
|
|
20
|
+
const { maxQuestions = MAX_QUESTIONS, customInputPrompt } = options;
|
|
21
|
+
const session = {
|
|
22
|
+
section: questions[0]?.section || 'product',
|
|
23
|
+
questionsAsked: [],
|
|
24
|
+
answers: [],
|
|
25
|
+
autoGenerateRequested: false,
|
|
26
|
+
startTime: new Date().toISOString(),
|
|
27
|
+
endTime: '',
|
|
28
|
+
};
|
|
29
|
+
for (let i = 0; i < Math.min(questions.length, maxQuestions); i++) {
|
|
30
|
+
const question = questions[i];
|
|
31
|
+
session.questionsAsked.push(question);
|
|
32
|
+
// Get user response
|
|
33
|
+
let selections;
|
|
34
|
+
let isValid = false;
|
|
35
|
+
// Keep asking until valid response
|
|
36
|
+
while (!isValid) {
|
|
37
|
+
selections = await responder(question, i + 1);
|
|
38
|
+
isValid = validateSelection(selections.join(','), question.type);
|
|
39
|
+
}
|
|
40
|
+
const parsedSelections = parseSelection(selections.join(','), question.type);
|
|
41
|
+
// Map selections to actual option text
|
|
42
|
+
const selectedOptions = parsedSelections.map(letter => {
|
|
43
|
+
const index = letter.charCodeAt(0) - 'A'.charCodeAt(0);
|
|
44
|
+
return question.options[index];
|
|
45
|
+
});
|
|
46
|
+
const answer = {
|
|
47
|
+
questionId: question.id,
|
|
48
|
+
selections: parsedSelections,
|
|
49
|
+
selectedOptions,
|
|
50
|
+
timestamp: new Date().toISOString(),
|
|
51
|
+
};
|
|
52
|
+
// Handle option D (custom input)
|
|
53
|
+
if (parsedSelections.includes('D') && customInputPrompt) {
|
|
54
|
+
answer.customText = await customInputPrompt(question);
|
|
55
|
+
}
|
|
56
|
+
session.answers.push(answer);
|
|
57
|
+
// Handle option E (auto-generate) - stop asking questions
|
|
58
|
+
if (parsedSelections.includes('E')) {
|
|
59
|
+
session.autoGenerateRequested = true;
|
|
60
|
+
session.autoGenerateAtQuestion = i;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
session.endTime = new Date().toISOString();
|
|
65
|
+
return session;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Draft document content from user's selected answers
|
|
69
|
+
* CRITICAL: Only use selected options, ignore questions and unselected options
|
|
70
|
+
*/
|
|
71
|
+
export function draftDocumentFromAnswers(session, analysis) {
|
|
72
|
+
const sections = [];
|
|
73
|
+
let inferredFromContext = session.autoGenerateRequested;
|
|
74
|
+
// Build content from selected answers
|
|
75
|
+
for (const answer of session.answers) {
|
|
76
|
+
if (answer.customText) {
|
|
77
|
+
// Option D: Use custom text directly
|
|
78
|
+
sections.push(answer.customText);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Options A, B, C: Use selected option text
|
|
82
|
+
for (const option of answer.selectedOptions) {
|
|
83
|
+
// Skip option D (Custom) and option E (Auto-generate)
|
|
84
|
+
if (!option.includes('custom') && !option.includes('Auto-generate')) {
|
|
85
|
+
sections.push(option);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// If auto-generate was requested, infer additional content from context
|
|
91
|
+
if (session.autoGenerateRequested) {
|
|
92
|
+
sections.push(...inferAdditionalContent(session, analysis));
|
|
93
|
+
}
|
|
94
|
+
// Generate document structure based on section type
|
|
95
|
+
const content = formatDocumentContent(session.section, sections, analysis);
|
|
96
|
+
return {
|
|
97
|
+
content,
|
|
98
|
+
inferredFromContext,
|
|
99
|
+
sectionsIncluded: sections,
|
|
100
|
+
wordCount: content.split(/\s+/).length,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Infer additional content when auto-generate is requested
|
|
105
|
+
*/
|
|
106
|
+
function inferAdditionalContent(session, analysis) {
|
|
107
|
+
const inferred = [];
|
|
108
|
+
// Use codebase analysis - check both languages and frameworks
|
|
109
|
+
const hasCodebaseData = Object.keys(analysis.languages).length > 0 ||
|
|
110
|
+
analysis.frameworks.frontend.length > 0 ||
|
|
111
|
+
analysis.frameworks.backend.length > 0;
|
|
112
|
+
if (hasCodebaseData) {
|
|
113
|
+
// Infer from detected technologies
|
|
114
|
+
const languages = Object.keys(analysis.languages);
|
|
115
|
+
if (languages.length > 0) {
|
|
116
|
+
inferred.push(`Technologies: ${languages.join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
if (analysis.frameworks.frontend.length > 0) {
|
|
119
|
+
inferred.push(`Frontend: ${analysis.frameworks.frontend.join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
if (analysis.frameworks.backend.length > 0) {
|
|
122
|
+
inferred.push(`Backend: ${analysis.frameworks.backend.join(', ')}`);
|
|
123
|
+
}
|
|
124
|
+
if (analysis.databases.length > 0) {
|
|
125
|
+
inferred.push(`Databases: ${analysis.databases.join(', ')}`);
|
|
126
|
+
}
|
|
127
|
+
if (analysis.architecture.length > 0 && !analysis.architecture.includes('unknown')) {
|
|
128
|
+
inferred.push(`Architecture: ${analysis.architecture.join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Infer from previous answers in session
|
|
132
|
+
for (const answer of session.answers) {
|
|
133
|
+
for (const option of answer.selectedOptions) {
|
|
134
|
+
if (!option.includes('Auto-generate') && !option.includes('custom')) {
|
|
135
|
+
// Use selected options as context for inference
|
|
136
|
+
inferred.push(option);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return inferred;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Format document content based on section type
|
|
144
|
+
*/
|
|
145
|
+
function formatDocumentContent(section, contentSections, analysis) {
|
|
146
|
+
const lines = [];
|
|
147
|
+
// Ensure minimum content for greenfield projects
|
|
148
|
+
const hasContent = contentSections.length > 0;
|
|
149
|
+
if (!hasContent) {
|
|
150
|
+
contentSections.push('To be defined during project setup.');
|
|
151
|
+
}
|
|
152
|
+
// Section-specific formatting
|
|
153
|
+
switch (section) {
|
|
154
|
+
case 'product':
|
|
155
|
+
lines.push('# Product Guide', '');
|
|
156
|
+
lines.push('## Overview', '');
|
|
157
|
+
lines.push(...contentSections);
|
|
158
|
+
if (analysis.projectGoal) {
|
|
159
|
+
lines.push('', `## Project Goal`, '', analysis.projectGoal);
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case 'guidelines':
|
|
163
|
+
lines.push('# Product Guidelines', '');
|
|
164
|
+
lines.push('## Guidelines', '');
|
|
165
|
+
lines.push(...contentSections);
|
|
166
|
+
break;
|
|
167
|
+
case 'tech-stack':
|
|
168
|
+
lines.push('# Tech Stack', '');
|
|
169
|
+
lines.push('## Technologies', '');
|
|
170
|
+
lines.push(...contentSections);
|
|
171
|
+
break;
|
|
172
|
+
case 'styleguides':
|
|
173
|
+
lines.push('# Code Style Guides', '');
|
|
174
|
+
lines.push('## Style Guidelines', '');
|
|
175
|
+
lines.push(...contentSections);
|
|
176
|
+
break;
|
|
177
|
+
case 'workflow':
|
|
178
|
+
lines.push('# Workflow', '');
|
|
179
|
+
lines.push('## Development Workflow', '');
|
|
180
|
+
lines.push(...contentSections);
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
lines.push('# Document', '');
|
|
184
|
+
lines.push(...contentSections);
|
|
185
|
+
}
|
|
186
|
+
return lines.join('\n');
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Save document to file and update state
|
|
190
|
+
*/
|
|
191
|
+
export async function saveDocumentState(state) {
|
|
192
|
+
try {
|
|
193
|
+
// Ensure directory exists
|
|
194
|
+
const dir = path.dirname(state.filePath);
|
|
195
|
+
if (!fs.existsSync(dir)) {
|
|
196
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
197
|
+
}
|
|
198
|
+
// Save document to file
|
|
199
|
+
fs.writeFileSync(state.filePath, state.content, 'utf-8');
|
|
200
|
+
// Compute state file path from outputPath (e.g., /path/to/conductor-cdd/product.md → /path/to/conductor-cdd/setup_state.json)
|
|
201
|
+
const stateFilePath = path.join(path.dirname(state.filePath), 'setup_state.json');
|
|
202
|
+
// Update state file
|
|
203
|
+
let setupState = {};
|
|
204
|
+
if (fs.existsSync(stateFilePath)) {
|
|
205
|
+
const existingState = fs.readFileSync(stateFilePath, 'utf-8');
|
|
206
|
+
setupState = JSON.parse(existingState);
|
|
207
|
+
}
|
|
208
|
+
const previousCheckpoints = setupState.last_successful_step
|
|
209
|
+
? [setupState.last_successful_step]
|
|
210
|
+
: [];
|
|
211
|
+
setupState.last_successful_step = state.checkpoint;
|
|
212
|
+
setupState[`${state.section}_timestamp`] = state.timestamp || new Date().toISOString();
|
|
213
|
+
fs.writeFileSync(stateFilePath, JSON.stringify(setupState, null, 2), 'utf-8');
|
|
214
|
+
return {
|
|
215
|
+
success: true,
|
|
216
|
+
stateFile: stateFilePath,
|
|
217
|
+
checkpoint: state.checkpoint,
|
|
218
|
+
filePath: state.filePath,
|
|
219
|
+
previousCheckpoints,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
return {
|
|
224
|
+
success: false,
|
|
225
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Orchestrate full document generation workflow
|
|
231
|
+
*/
|
|
232
|
+
export async function generateDocument(options) {
|
|
233
|
+
const { section, questions, analysis, responder, approvalFlow, outputPath, maxRevisions = MAX_REVISION_ATTEMPTS, customInputPrompt } = options;
|
|
234
|
+
try {
|
|
235
|
+
// Validate questions array
|
|
236
|
+
if (!questions || questions.length === 0) {
|
|
237
|
+
return {
|
|
238
|
+
success: false,
|
|
239
|
+
error: `No questions generated for section: ${section}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Step 1: Present questions sequentially
|
|
243
|
+
const session = await presentQuestionsSequentially(questions, responder, {
|
|
244
|
+
customInputPrompt,
|
|
245
|
+
});
|
|
246
|
+
// Step 2: Draft document from answers
|
|
247
|
+
let draft = draftDocumentFromAnswers(session, analysis);
|
|
248
|
+
let revisionCount = 0;
|
|
249
|
+
let approved = false;
|
|
250
|
+
let finalContent = draft.content;
|
|
251
|
+
// Step 3: Approval/revision loop
|
|
252
|
+
while (!approved && revisionCount < maxRevisions) {
|
|
253
|
+
const approvalResult = await approvalFlow(draft);
|
|
254
|
+
if (approvalResult.approved) {
|
|
255
|
+
approved = true;
|
|
256
|
+
finalContent = approvalResult.finalContent || draft.content;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Revise based on guidance
|
|
260
|
+
revisionCount++;
|
|
261
|
+
if (revisionCount >= maxRevisions) {
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
error: `Max revision attempts (${maxRevisions}) exceeded`,
|
|
265
|
+
revisionCount,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// Re-draft with revision guidance
|
|
269
|
+
// In real implementation, this would incorporate the revision guidance
|
|
270
|
+
draft = draftDocumentFromAnswers(session, analysis);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Step 4: Save document and update state
|
|
274
|
+
const checkpoint = CHECKPOINT_MAP[section];
|
|
275
|
+
const saveResult = await saveDocumentState({
|
|
276
|
+
section,
|
|
277
|
+
checkpoint,
|
|
278
|
+
content: finalContent,
|
|
279
|
+
filePath: outputPath,
|
|
280
|
+
timestamp: new Date().toISOString(),
|
|
281
|
+
});
|
|
282
|
+
if (!saveResult.success) {
|
|
283
|
+
return {
|
|
284
|
+
success: false,
|
|
285
|
+
error: `Failed to save document: ${saveResult.error}`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
success: true,
|
|
290
|
+
checkpoint,
|
|
291
|
+
autoGenerated: session.autoGenerateRequested,
|
|
292
|
+
revisionCount,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|