@iservu-inc/adf-cli 0.17.0 ā 0.17.5
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/.context/memory/architecture.md +1 -1
- package/.context/memory/glossary.md +1 -1
- package/CLAUDE.md +5 -5
- package/README.md +1 -1
- package/lib/analysis/heuristic-gap-analyzer.js +4 -4
- package/lib/analysis/synthesis-engine.js +5 -5
- package/lib/commands/init.js +545 -467
- package/lib/frameworks/output-generators.js +54 -147
- package/lib/frameworks/progress-tracker.js +16 -0
- package/lib/frameworks/questions.js +156 -464
- package/lib/frameworks/session-manager.js +56 -0
- package/lib/learning/analytics-view.js +5 -5
- package/lib/learning/analytics.js +22 -6
- package/lib/templates/scripts/analyze-docs.js +23 -12
- package/lib/templates/scripts/build.js +1 -1
- package/lib/templates/scripts/check-framework-updates.js +1 -1
- package/lib/templates/scripts/init.js +1 -1
- package/lib/templates/shared/memory/constitution.md +2 -2
- package/lib/templates/shared/templates/README.md +2 -2
- package/lib/utils/context-extractor.js +51 -3
- package/lib/utils/framework-detector.js +11 -2
- package/package.json +1 -1
- package/tests/analytics-view.test.js +12 -10
- package/tests/context-extractor.test.js +47 -2
- package/tests/decay-manager.test.js +22 -19
- package/tests/deploy.test.js +7 -3
- package/tests/dynamic-question-generator.test.js +2 -2
- package/tests/framework-detector.test.js +31 -3
- package/tests/heuristic-gap-analyzer.test.js +5 -5
- package/tests/pattern-decay.test.js +34 -52
- package/tests/session-manager.test.js +125 -0
package/lib/commands/init.js
CHANGED
|
@@ -1,467 +1,545 @@
|
|
|
1
|
-
const fs = require('fs-extra');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const inquirer = require('inquirer');
|
|
4
|
-
const chalk = require('chalk');
|
|
5
|
-
const ora = require('ora');
|
|
6
|
-
const {
|
|
7
|
-
detectProjectType,
|
|
8
|
-
getWorkflowRecommendation
|
|
9
|
-
} = require('../utils/project-detector');
|
|
10
|
-
const FrameworkDetector = require('../utils/framework-detector');
|
|
11
|
-
const ContextExtractor = require('../utils/context-extractor');
|
|
12
|
-
const SynthesisEngine = require('../analysis/synthesis-engine');
|
|
13
|
-
const HeuristicGapAnalyzer = require('../analysis/heuristic-gap-analyzer');
|
|
14
|
-
const AIGapAnalyzer = require('../analysis/ai-gap-analyzer');
|
|
15
|
-
const DynamicQuestionGenerator = require('../analysis/dynamic-question-generator');
|
|
16
|
-
const Interviewer = require('../frameworks/interviewer');
|
|
17
|
-
const SessionManager = require('../frameworks/session-manager');
|
|
18
|
-
const { deployToTool } = require('./deploy');
|
|
19
|
-
const { configureAIProvider, loadEnvIntoProcess, getEnvFilePath } = require('../ai/ai-config');
|
|
20
|
-
|
|
21
|
-
async function init(options) {
|
|
22
|
-
console.log(chalk.cyan.bold('\nš AgentDevFramework - Software Development Requirements\n'));
|
|
23
|
-
|
|
24
|
-
const cwd = process.cwd();
|
|
25
|
-
const adfDir = path.join(cwd, '.adf');
|
|
26
|
-
|
|
27
|
-
// Load .env file if it exists (for API keys)
|
|
28
|
-
const envPath = getEnvFilePath(cwd);
|
|
29
|
-
if (await fs.pathExists(envPath)) {
|
|
30
|
-
loadEnvIntoProcess(envPath);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Detect existing frameworks for potential synthesis
|
|
34
|
-
const detectedFrameworks = await FrameworkDetector.detect(cwd);
|
|
35
|
-
const hasOtherFrameworks = detectedFrameworks.filter(f => f !== 'adf').length > 0;
|
|
36
|
-
|
|
37
|
-
if (hasOtherFrameworks) {
|
|
38
|
-
const frameworkNames = {
|
|
39
|
-
'agent-native': 'Agent-Native',
|
|
40
|
-
'openspec': 'OpenSpec',
|
|
41
|
-
'specification-driven': 'Specification-Driven'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
console.log(chalk.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
sessionPath,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
sessionPath,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
console.log(chalk.yellow(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
console.log(chalk.yellow('
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
console.log(chalk.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const {
|
|
7
|
+
detectProjectType,
|
|
8
|
+
getWorkflowRecommendation
|
|
9
|
+
} = require('../utils/project-detector');
|
|
10
|
+
const FrameworkDetector = require('../utils/framework-detector');
|
|
11
|
+
const ContextExtractor = require('../utils/context-extractor');
|
|
12
|
+
const SynthesisEngine = require('../analysis/synthesis-engine');
|
|
13
|
+
const HeuristicGapAnalyzer = require('../analysis/heuristic-gap-analyzer');
|
|
14
|
+
const AIGapAnalyzer = require('../analysis/ai-gap-analyzer');
|
|
15
|
+
const DynamicQuestionGenerator = require('../analysis/dynamic-question-generator');
|
|
16
|
+
const Interviewer = require('../frameworks/interviewer');
|
|
17
|
+
const SessionManager = require('../frameworks/session-manager');
|
|
18
|
+
const { deployToTool } = require('./deploy');
|
|
19
|
+
const { configureAIProvider, loadEnvIntoProcess, getEnvFilePath } = require('../ai/ai-config');
|
|
20
|
+
|
|
21
|
+
async function init(options) {
|
|
22
|
+
console.log(chalk.cyan.bold('\nš AgentDevFramework - Software Development Requirements\n'));
|
|
23
|
+
|
|
24
|
+
const cwd = process.cwd();
|
|
25
|
+
const adfDir = path.join(cwd, '.adf');
|
|
26
|
+
|
|
27
|
+
// Load .env file if it exists (for API keys)
|
|
28
|
+
const envPath = getEnvFilePath(cwd);
|
|
29
|
+
if (await fs.pathExists(envPath)) {
|
|
30
|
+
loadEnvIntoProcess(envPath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Detect existing frameworks for potential synthesis
|
|
34
|
+
const detectedFrameworks = await FrameworkDetector.detect(cwd);
|
|
35
|
+
const hasOtherFrameworks = detectedFrameworks.filter(f => f !== 'adf').length > 0;
|
|
36
|
+
|
|
37
|
+
if (hasOtherFrameworks) {
|
|
38
|
+
const frameworkNames = {
|
|
39
|
+
'agent-native': 'Agent-Native',
|
|
40
|
+
'openspec': 'OpenSpec',
|
|
41
|
+
'specification-driven': 'Specification-Driven',
|
|
42
|
+
'gemini-conductor': 'Gemini CLI Conductor'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const displayNames = detectedFrameworks
|
|
46
|
+
.filter(f => f !== 'adf')
|
|
47
|
+
.map(f => frameworkNames[f] || f);
|
|
48
|
+
|
|
49
|
+
console.log(chalk.cyan('š¦ Existing Development Frameworks Detected:'));
|
|
50
|
+
console.log(chalk.gray(` ${displayNames.join(', ')}\n`));
|
|
51
|
+
|
|
52
|
+
const { action } = await inquirer.prompt([
|
|
53
|
+
{
|
|
54
|
+
type: 'list',
|
|
55
|
+
name: 'action',
|
|
56
|
+
message: 'How would you like to proceed?',
|
|
57
|
+
choices: [
|
|
58
|
+
{
|
|
59
|
+
name: 'Synthesize & Augment (Seamless Merge - Recommended)',
|
|
60
|
+
value: 'synthesize'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'Start Fresh (Ignore existing frameworks)',
|
|
64
|
+
value: 'fresh'
|
|
65
|
+
},
|
|
66
|
+
new inquirer.Separator(),
|
|
67
|
+
{
|
|
68
|
+
name: chalk.gray('ā Exit'),
|
|
69
|
+
value: 'exit'
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
if (action === 'exit') return;
|
|
76
|
+
|
|
77
|
+
if (action === 'synthesize') {
|
|
78
|
+
const spinner = ora('Synthesizing project context...').start();
|
|
79
|
+
try {
|
|
80
|
+
const context = await ContextExtractor.extract(cwd, detectedFrameworks);
|
|
81
|
+
const sessionPath = await SynthesisEngine.createAugmentedSession(cwd, context, 'balanced');
|
|
82
|
+
spinner.succeed(chalk.green(`ā Project synthesized into new session: ${path.basename(sessionPath)}`));
|
|
83
|
+
|
|
84
|
+
console.log(chalk.yellow('\nš” Starting Adaptive Augmentation Interview to fill knowledge gaps...\n'));
|
|
85
|
+
|
|
86
|
+
// Load AI config for the interviewer
|
|
87
|
+
const aiConfig = await configureAIProvider(cwd);
|
|
88
|
+
const sessionProgress = await fs.readJson(path.join(sessionPath, '_progress.json'));
|
|
89
|
+
|
|
90
|
+
// Perform Knowledge Gap Analysis
|
|
91
|
+
console.log(chalk.cyan('š Analyzing for knowledge gaps...'));
|
|
92
|
+
const hGaps = HeuristicGapAnalyzer.analyze(context, 'balanced');
|
|
93
|
+
|
|
94
|
+
let aiGaps = [];
|
|
95
|
+
if (aiConfig) {
|
|
96
|
+
const AIClient = require('../ai/ai-client');
|
|
97
|
+
const aiClient = new AIClient(aiConfig);
|
|
98
|
+
const aiAnalyzer = new AIGapAnalyzer(aiClient);
|
|
99
|
+
aiGaps = await aiAnalyzer.analyze(context);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const dynamicQuestions = DynamicQuestionGenerator.generate(hGaps, aiGaps);
|
|
103
|
+
|
|
104
|
+
if (dynamicQuestions.length === 0) {
|
|
105
|
+
console.log(chalk.green('ā Context is comprehensive. No additional questions needed.'));
|
|
106
|
+
const interviewer = new Interviewer('balanced', cwd, {
|
|
107
|
+
sessionId: path.basename(sessionPath),
|
|
108
|
+
sessionPath,
|
|
109
|
+
progress: sessionProgress
|
|
110
|
+
}, aiConfig);
|
|
111
|
+
await interviewer.generateOutputs();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(chalk.yellow(`\nš” Identified ${dynamicQuestions.length} gaps. Starting targeted interview...\n`));
|
|
116
|
+
|
|
117
|
+
const interviewer = new Interviewer('balanced', cwd, {
|
|
118
|
+
sessionId: path.basename(sessionPath),
|
|
119
|
+
sessionPath,
|
|
120
|
+
progress: sessionProgress
|
|
121
|
+
}, aiConfig, dynamicQuestions);
|
|
122
|
+
|
|
123
|
+
await interviewer.start();
|
|
124
|
+
return;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
spinner.fail(chalk.red(`Synthesis failed: ${error.message}`));
|
|
127
|
+
console.log(chalk.yellow('Falling back to standard initialization...\n'));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for resumable sessions FIRST (before asking to overwrite)
|
|
133
|
+
const sessionManager = new SessionManager(cwd);
|
|
134
|
+
const existingSession = await sessionManager.promptToResume();
|
|
135
|
+
|
|
136
|
+
if (existingSession) {
|
|
137
|
+
// Resume existing session
|
|
138
|
+
// Check if session has AI config (from resumed session)
|
|
139
|
+
let aiConfig = existingSession.progress.aiConfig;
|
|
140
|
+
|
|
141
|
+
if (aiConfig) {
|
|
142
|
+
// We have AI config from session, but need to verify API key exists
|
|
143
|
+
const apiKey = process.env[aiConfig.envVar];
|
|
144
|
+
if (!apiKey) {
|
|
145
|
+
console.log(chalk.yellow(`\nā ļø Previous session used ${aiConfig.providerName}`));
|
|
146
|
+
console.log(chalk.yellow(`Please configure API key to resume...\n`));
|
|
147
|
+
aiConfig = await configureAIProvider(cwd);
|
|
148
|
+
} else {
|
|
149
|
+
// Add API key to config (it's not stored in session for security)
|
|
150
|
+
aiConfig.apiKey = apiKey;
|
|
151
|
+
console.log(chalk.green(`\nā Resuming with ${aiConfig.providerName} (${aiConfig.model})\n`));
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
// Old session without AI config, configure now
|
|
155
|
+
console.log(chalk.yellow('\nā ļø This session was created before AI provider integration.'));
|
|
156
|
+
console.log(chalk.yellow('Please configure AI provider to continue...\n'));
|
|
157
|
+
aiConfig = await configureAIProvider(cwd);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const interviewer = new Interviewer(existingSession.progress.framework || 'balanced', cwd, existingSession, aiConfig);
|
|
161
|
+
const sessionPath = await interviewer.start();
|
|
162
|
+
|
|
163
|
+
if (sessionPath === 'back') {
|
|
164
|
+
// User requested to go back to main menu
|
|
165
|
+
// Fall through to existing content check
|
|
166
|
+
} else {
|
|
167
|
+
console.log(chalk.green.bold('\n⨠Requirements gathering complete!\n'));
|
|
168
|
+
console.log(chalk.cyan(`š Session saved to: ${sessionPath}\n`));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check if already initialized with actual content
|
|
174
|
+
if (await fs.pathExists(adfDir)) {
|
|
175
|
+
// Check for meaningful content (not just .env file)
|
|
176
|
+
const hasContent = await hasMeaningfulContent(adfDir);
|
|
177
|
+
|
|
178
|
+
if (hasContent) {
|
|
179
|
+
// Show what exists
|
|
180
|
+
const existingContent = await getExistingContent(adfDir);
|
|
181
|
+
console.log(chalk.cyan('\nš¦ Existing ADF Project Detected\n'));
|
|
182
|
+
|
|
183
|
+
if (existingContent.sessions > 0) {
|
|
184
|
+
console.log(chalk.gray(` Sessions: ${existingContent.sessions} session(s)`));
|
|
185
|
+
}
|
|
186
|
+
if (existingContent.outputs > 0) {
|
|
187
|
+
console.log(chalk.gray(` Outputs: ${existingContent.outputs} file(s)`));
|
|
188
|
+
}
|
|
189
|
+
if (existingContent.learning) {
|
|
190
|
+
console.log(chalk.gray(` Learning data: Present`));
|
|
191
|
+
}
|
|
192
|
+
console.log('');
|
|
193
|
+
|
|
194
|
+
const { action } = await inquirer.prompt([
|
|
195
|
+
{
|
|
196
|
+
type: 'list',
|
|
197
|
+
name: 'action',
|
|
198
|
+
message: 'What would you like to do?',
|
|
199
|
+
choices: [
|
|
200
|
+
{
|
|
201
|
+
name: 'Continue with Existing Project',
|
|
202
|
+
value: 'continue',
|
|
203
|
+
short: 'Continue'
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'Reset this Project (delete all data)',
|
|
207
|
+
value: 'reset',
|
|
208
|
+
short: 'Reset'
|
|
209
|
+
},
|
|
210
|
+
new inquirer.Separator(),
|
|
211
|
+
{
|
|
212
|
+
name: chalk.gray('ā Don\'t change & Exit'),
|
|
213
|
+
value: 'exit',
|
|
214
|
+
short: 'Exit'
|
|
215
|
+
}
|
|
216
|
+
],
|
|
217
|
+
default: 'continue'
|
|
218
|
+
}
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
if (action === 'exit') {
|
|
222
|
+
console.log(chalk.gray('\nā Exited without changes.\n'));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (action === 'reset') {
|
|
227
|
+
// Confirm deletion
|
|
228
|
+
const { confirmReset } = await inquirer.prompt([
|
|
229
|
+
{
|
|
230
|
+
type: 'confirm',
|
|
231
|
+
name: 'confirmReset',
|
|
232
|
+
message: chalk.red('ā ļø This will permanently delete all sessions and data. Continue?'),
|
|
233
|
+
default: false
|
|
234
|
+
}
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
if (!confirmReset) {
|
|
238
|
+
console.log(chalk.gray('\nā Reset cancelled. Exited without changes.\n'));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await fs.remove(adfDir);
|
|
243
|
+
console.log(chalk.yellow('\nā Project reset. Starting fresh...\n'));
|
|
244
|
+
} else if (action === 'continue') {
|
|
245
|
+
// Continue with existing project - show sessions with details
|
|
246
|
+
console.log(chalk.green('\nā Continuing with existing project...\n'));
|
|
247
|
+
|
|
248
|
+
const detailedSessions = await sessionManager.getSessionsWithDetails();
|
|
249
|
+
|
|
250
|
+
// Build session choices: in-progress first, then completed-with-gaps
|
|
251
|
+
const resumable = detailedSessions
|
|
252
|
+
.filter(s => s.progress.status === 'in-progress' && s.progress.canResume)
|
|
253
|
+
.sort((a, b) => new Date(b.progress.lastUpdated) - new Date(a.progress.lastUpdated));
|
|
254
|
+
|
|
255
|
+
const withGaps = detailedSessions
|
|
256
|
+
.filter(s => s.progress.status === 'completed' && s.hasGaps)
|
|
257
|
+
.sort((a, b) => new Date(b.progress.lastUpdated) - new Date(a.progress.lastUpdated));
|
|
258
|
+
|
|
259
|
+
const sessionChoices = [];
|
|
260
|
+
|
|
261
|
+
for (const s of resumable) {
|
|
262
|
+
const framework = (s.progress.framework || 'rapid').toUpperCase();
|
|
263
|
+
const date = new Date(s.progress.lastUpdated).toLocaleDateString();
|
|
264
|
+
const pct = s.totalQuestions > 0
|
|
265
|
+
? Math.round((s.answeredCount / s.totalQuestions) * 100) : 0;
|
|
266
|
+
sessionChoices.push({
|
|
267
|
+
name: `Resume: ${framework} | ${date} | ${pct}% complete (${s.answeredCount}/${s.totalQuestions} questions)`,
|
|
268
|
+
value: { action: 'resume', sessionId: s.sessionId },
|
|
269
|
+
short: `Resume ${s.sessionId}`
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const s of withGaps) {
|
|
274
|
+
const framework = (s.progress.framework || 'rapid').toUpperCase();
|
|
275
|
+
const date = new Date(s.progress.lastUpdated).toLocaleDateString();
|
|
276
|
+
sessionChoices.push({
|
|
277
|
+
name: `Fill gaps: ${framework} | ${date} | ${s.unansweredCount} unanswered questions`,
|
|
278
|
+
value: { action: 'fill-gaps', sessionId: s.sessionId },
|
|
279
|
+
short: `Fill gaps ${s.sessionId}`
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (sessionChoices.length > 0) {
|
|
284
|
+
sessionChoices.push(new inquirer.Separator());
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
sessionChoices.push({
|
|
288
|
+
name: 'Start a new session (keeps existing data)',
|
|
289
|
+
value: { action: 'new-session' },
|
|
290
|
+
short: 'New Session'
|
|
291
|
+
});
|
|
292
|
+
sessionChoices.push({
|
|
293
|
+
name: chalk.gray('ā Exit'),
|
|
294
|
+
value: { action: 'exit' },
|
|
295
|
+
short: 'Exit'
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const { continueChoice } = await inquirer.prompt([
|
|
299
|
+
{
|
|
300
|
+
type: 'list',
|
|
301
|
+
name: 'continueChoice',
|
|
302
|
+
message: 'Select a session or start new:',
|
|
303
|
+
choices: sessionChoices
|
|
304
|
+
}
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
if (continueChoice.action === 'exit') {
|
|
308
|
+
console.log(chalk.gray('\nā Exited.\n'));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (continueChoice.action === 'resume' || continueChoice.action === 'fill-gaps') {
|
|
313
|
+
let selectedSession;
|
|
314
|
+
|
|
315
|
+
if (continueChoice.action === 'fill-gaps') {
|
|
316
|
+
// Reopen completed session so interviewer treats it as resumable
|
|
317
|
+
selectedSession = await sessionManager.reopenSession(continueChoice.sessionId);
|
|
318
|
+
} else {
|
|
319
|
+
selectedSession = detailedSessions.find(s => s.sessionId === continueChoice.sessionId);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Configure AI provider (reuse pattern from session resume above)
|
|
323
|
+
let aiConfig = selectedSession.progress.aiConfig;
|
|
324
|
+
|
|
325
|
+
if (aiConfig) {
|
|
326
|
+
const apiKey = process.env[aiConfig.envVar];
|
|
327
|
+
if (!apiKey) {
|
|
328
|
+
console.log(chalk.yellow(`\nā ļø Previous session used ${aiConfig.providerName}`));
|
|
329
|
+
console.log(chalk.yellow(`Please configure API key to continue...\n`));
|
|
330
|
+
aiConfig = await configureAIProvider(cwd);
|
|
331
|
+
} else {
|
|
332
|
+
aiConfig.apiKey = apiKey;
|
|
333
|
+
console.log(chalk.green(`\nā Resuming with ${aiConfig.providerName} (${aiConfig.model})\n`));
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
console.log(chalk.yellow('\nPlease configure AI provider to continue...\n'));
|
|
337
|
+
aiConfig = await configureAIProvider(cwd);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const interviewer = new Interviewer(
|
|
341
|
+
selectedSession.progress.framework || 'balanced',
|
|
342
|
+
cwd,
|
|
343
|
+
selectedSession,
|
|
344
|
+
aiConfig
|
|
345
|
+
);
|
|
346
|
+
const sessionPath = await interviewer.start();
|
|
347
|
+
|
|
348
|
+
if (sessionPath !== 'back') {
|
|
349
|
+
console.log(chalk.green.bold('\n⨠Requirements gathering complete!\n'));
|
|
350
|
+
console.log(chalk.cyan(`š Session saved to: ${sessionPath}\n`));
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// new-session: fall through to workflow selection below
|
|
356
|
+
console.log('');
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
// Only .env file exists - safe to continue without prompting
|
|
360
|
+
console.log(chalk.gray('ā Using existing .adf directory\n'));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Detect project type
|
|
365
|
+
const spinner = ora('Detecting project type...').start();
|
|
366
|
+
const projectType = await detectProjectType(cwd);
|
|
367
|
+
spinner.succeed(`Project type: ${chalk.green(projectType.type)}`);
|
|
368
|
+
|
|
369
|
+
// Configure AI provider BEFORE workflow selection
|
|
370
|
+
console.log('');
|
|
371
|
+
const aiConfig = await configureAIProvider(cwd);
|
|
372
|
+
|
|
373
|
+
// Determine workflow/framework
|
|
374
|
+
let workflow;
|
|
375
|
+
|
|
376
|
+
if (options.rapid) {
|
|
377
|
+
workflow = 'rapid';
|
|
378
|
+
console.log(chalk.blue('\nUsing: Level 1: Rapid (Agent-Native) - from --rapid flag'));
|
|
379
|
+
} else if (options.balanced) {
|
|
380
|
+
workflow = 'balanced';
|
|
381
|
+
console.log(chalk.blue('\nUsing: Level 2: Balanced (OpenSpec) - from --balanced flag'));
|
|
382
|
+
} else if (options.comprehensive) {
|
|
383
|
+
workflow = 'comprehensive';
|
|
384
|
+
console.log(chalk.blue('\nUsing: Level 3: Comprehensive (Agent-Native) - from --comprehensive flag'));
|
|
385
|
+
} else {
|
|
386
|
+
// Interactive workflow selection
|
|
387
|
+
workflow = await getWorkflowRecommendation(projectType);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Create .adf directory
|
|
391
|
+
await fs.ensureDir(adfDir);
|
|
392
|
+
|
|
393
|
+
// AI already configured above - pass to interviewer
|
|
394
|
+
// Start interview
|
|
395
|
+
const interviewer = new Interviewer(workflow, cwd, null, aiConfig);
|
|
396
|
+
const sessionPath = await interviewer.start();
|
|
397
|
+
|
|
398
|
+
// Show completion message
|
|
399
|
+
console.log(chalk.cyan('š Requirements Complete!\n'));
|
|
400
|
+
console.log(chalk.gray(` ā Files saved to: ${sessionPath}/outputs/`));
|
|
401
|
+
console.log(chalk.gray(` ā You can review your requirements anytime\n`));
|
|
402
|
+
|
|
403
|
+
// Generate A2A agent cards
|
|
404
|
+
try {
|
|
405
|
+
const { generateA2A } = require('../generators');
|
|
406
|
+
await generateA2A(sessionPath, cwd, workflow);
|
|
407
|
+
console.log(chalk.gray(' ā A2A agent cards generated'));
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.warn(chalk.yellow(` ā Could not generate A2A cards: ${error.message}`));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Optional: Deploy to tool
|
|
413
|
+
if (options.tool) {
|
|
414
|
+
console.log('');
|
|
415
|
+
await deployToTool(options.tool, { silent: false });
|
|
416
|
+
} else {
|
|
417
|
+
const { deployNow } = await inquirer.prompt([
|
|
418
|
+
{
|
|
419
|
+
type: 'confirm',
|
|
420
|
+
name: 'deployNow',
|
|
421
|
+
message: 'Automatically deploy to your IDE? (I\'ll configure everything for you)',
|
|
422
|
+
default: true
|
|
423
|
+
}
|
|
424
|
+
]);
|
|
425
|
+
|
|
426
|
+
if (deployNow) {
|
|
427
|
+
const { tools } = await inquirer.prompt([
|
|
428
|
+
{
|
|
429
|
+
type: 'checkbox',
|
|
430
|
+
name: 'tools',
|
|
431
|
+
message: 'Select tools (space to select, enter to confirm):',
|
|
432
|
+
choices: [
|
|
433
|
+
{ name: 'Windsurf', value: 'windsurf' },
|
|
434
|
+
{ name: 'Cursor', value: 'cursor' },
|
|
435
|
+
{ name: 'VSCode/Copilot', value: 'vscode' },
|
|
436
|
+
{ name: 'Claude Code', value: 'claude-code' },
|
|
437
|
+
{ name: 'Gemini CLI', value: 'gemini-cli' }
|
|
438
|
+
],
|
|
439
|
+
validate: (answer) => {
|
|
440
|
+
if (answer.length === 0) {
|
|
441
|
+
return 'You must choose at least one tool.';
|
|
442
|
+
}
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
]);
|
|
447
|
+
|
|
448
|
+
// Deploy to each selected tool
|
|
449
|
+
for (const tool of tools) {
|
|
450
|
+
console.log('');
|
|
451
|
+
await deployToTool(tool, { silent: false });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
console.log(chalk.green.bold('\nā
All done! Happy coding! š\n'));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Check if .adf directory has meaningful content
|
|
461
|
+
* Returns true if there are session files, progress files, or outputs
|
|
462
|
+
* Returns false if only .env or empty
|
|
463
|
+
*/
|
|
464
|
+
async function hasMeaningfulContent(adfDir) {
|
|
465
|
+
try {
|
|
466
|
+
const contents = await fs.readdir(adfDir);
|
|
467
|
+
|
|
468
|
+
// Filter out .env and other config files that don't represent sessions
|
|
469
|
+
const meaningfulFiles = contents.filter(item => {
|
|
470
|
+
return item !== '.env' &&
|
|
471
|
+
item !== '.gitignore' &&
|
|
472
|
+
!item.startsWith('.');
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
if (meaningfulFiles.length === 0) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Check for session directories or files
|
|
480
|
+
for (const item of meaningfulFiles) {
|
|
481
|
+
const itemPath = path.join(adfDir, item);
|
|
482
|
+
const stats = await fs.stat(itemPath);
|
|
483
|
+
|
|
484
|
+
if (stats.isDirectory()) {
|
|
485
|
+
// Check if directory has files (sessions/, outputs/, etc.)
|
|
486
|
+
const dirContents = await fs.readdir(itemPath);
|
|
487
|
+
if (dirContents.length > 0) {
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
} else if (stats.isFile()) {
|
|
491
|
+
// Any non-config file indicates content
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return false;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
// If error reading, assume no content
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Get detailed information about existing ADF content
|
|
505
|
+
* Returns object with counts and details
|
|
506
|
+
*/
|
|
507
|
+
async function getExistingContent(adfDir) {
|
|
508
|
+
const result = {
|
|
509
|
+
sessions: 0,
|
|
510
|
+
outputs: 0,
|
|
511
|
+
learning: false,
|
|
512
|
+
details: []
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
const contents = await fs.readdir(adfDir);
|
|
517
|
+
|
|
518
|
+
for (const item of contents) {
|
|
519
|
+
const itemPath = path.join(adfDir, item);
|
|
520
|
+
const stats = await fs.stat(itemPath);
|
|
521
|
+
|
|
522
|
+
if (stats.isDirectory()) {
|
|
523
|
+
if (item === 'sessions') {
|
|
524
|
+
// Count session directories
|
|
525
|
+
const sessions = await fs.readdir(itemPath);
|
|
526
|
+
result.sessions = sessions.filter(s => !s.startsWith('.')).length;
|
|
527
|
+
} else if (item === 'outputs') {
|
|
528
|
+
// Count output files
|
|
529
|
+
const outputs = await fs.readdir(itemPath);
|
|
530
|
+
result.outputs = outputs.filter(o => !o.startsWith('.')).length;
|
|
531
|
+
} else if (item === 'learning') {
|
|
532
|
+
// Check for learning data
|
|
533
|
+
const learningFiles = await fs.readdir(itemPath);
|
|
534
|
+
result.learning = learningFiles.length > 0;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return result;
|
|
540
|
+
} catch (error) {
|
|
541
|
+
return result;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
module.exports = init;
|