@iservu-inc/adf-cli 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.project/chats/current/SESSION-STATUS.md +29 -27
  2. package/.project/docs/ROADMAP.md +74 -64
  3. package/CHANGELOG.md +78 -0
  4. package/CLAUDE.md +1 -1
  5. package/README.md +63 -27
  6. package/bin/adf.js +54 -0
  7. package/lib/analysis/dynamic-pipeline.js +26 -0
  8. package/lib/analysis/knowledge-graph.js +66 -0
  9. package/lib/commands/deploy.js +35 -0
  10. package/lib/commands/harness.js +345 -0
  11. package/lib/commands/init.js +135 -10
  12. package/lib/frameworks/interviewer.js +130 -0
  13. package/lib/frameworks/progress-tracker.js +30 -1
  14. package/lib/frameworks/session-manager.js +76 -0
  15. package/lib/harness/context-window-manager.js +255 -0
  16. package/lib/harness/event-logger.js +115 -0
  17. package/lib/harness/feature-manifest.js +175 -0
  18. package/lib/harness/headless-adapter.js +184 -0
  19. package/lib/harness/milestone-tracker.js +183 -0
  20. package/lib/harness/protocol.js +503 -0
  21. package/lib/harness/provider-bridge.js +226 -0
  22. package/lib/harness/run-manager.js +267 -0
  23. package/lib/templates/scripts/analyze-docs.js +12 -1
  24. package/lib/utils/context-extractor.js +48 -0
  25. package/lib/utils/framework-detector.js +10 -1
  26. package/lib/utils/project-detector.js +5 -1
  27. package/lib/utils/tool-detector.js +167 -0
  28. package/lib/utils/tool-feature-registry.js +82 -13
  29. package/lib/utils/tool-recommender.js +325 -0
  30. package/package.json +1 -1
  31. package/tests/context-extractor.test.js +45 -0
  32. package/tests/framework-detector.test.js +28 -0
  33. package/tests/harness-backward-compat.test.js +251 -0
  34. package/tests/harness-context-window.test.js +310 -0
  35. package/tests/harness-event-logger.test.js +148 -0
  36. package/tests/harness-feature-manifest.test.js +124 -0
  37. package/tests/harness-headless-adapter.test.js +196 -0
  38. package/tests/harness-integration.test.js +207 -0
  39. package/tests/harness-milestone-tracker.test.js +158 -0
  40. package/tests/harness-protocol.test.js +341 -0
  41. package/tests/harness-provider-bridge.test.js +180 -0
  42. package/tests/harness-provider-switch.test.js +204 -0
  43. package/tests/harness-run-manager.test.js +131 -0
  44. package/tests/tool-detector.test.js +152 -0
  45. package/tests/tool-recommender.test.js +218 -0
@@ -216,6 +216,72 @@ class KnowledgeGraph {
216
216
  };
217
217
  }
218
218
 
219
+ /**
220
+ * Export high-confidence items for handoff package (compact format)
221
+ */
222
+ toHandoffFormat(minConfidence = 70, maxContentLength = 200) {
223
+ const highConfidenceItems = [];
224
+ const typesSeen = [];
225
+
226
+ for (const [type, items] of this.knowledge.entries()) {
227
+ typesSeen.push(type);
228
+ for (const item of items) {
229
+ if (item.confidence >= minConfidence) {
230
+ highConfidenceItems.push({
231
+ type,
232
+ content: item.content.length > maxContentLength
233
+ ? item.content.slice(0, maxContentLength) + '...'
234
+ : item.content,
235
+ confidence: item.confidence,
236
+ sources: item.sources
237
+ });
238
+ }
239
+ }
240
+ }
241
+
242
+ return {
243
+ highConfidenceItems,
244
+ totalItems: this.getStats().totalItems,
245
+ typesSeen
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Restore knowledge from handoff format
251
+ */
252
+ fromHandoffFormat(data) {
253
+ if (!data || !data.highConfidenceItems) return;
254
+
255
+ for (const item of data.highConfidenceItems) {
256
+ this.add([{
257
+ type: item.type,
258
+ content: item.content,
259
+ confidence: item.confidence,
260
+ source: item.sources ? item.sources[0] : 'handoff'
261
+ }]);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Get high-confidence snapshot (items at confidence >= 80)
267
+ */
268
+ getHighConfidenceSnapshot() {
269
+ const snapshot = {};
270
+
271
+ for (const [type, items] of this.knowledge.entries()) {
272
+ const highConf = items.filter(i => i.confidence >= 80);
273
+ if (highConf.length > 0) {
274
+ snapshot[type] = highConf.map(i => ({
275
+ content: i.content,
276
+ confidence: i.confidence,
277
+ sources: i.sources
278
+ }));
279
+ }
280
+ }
281
+
282
+ return snapshot;
283
+ }
284
+
219
285
  /**
220
286
  * Clear all knowledge
221
287
  */
@@ -2,6 +2,8 @@ const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
  const chalk = require('chalk');
4
4
  const ora = require('ora');
5
+ const ToolDetector = require('../utils/tool-detector');
6
+ const ToolRecommender = require('../utils/tool-recommender');
5
7
  const {
6
8
  generateAgentsMd,
7
9
  generateWindsurf,
@@ -334,6 +336,39 @@ function getAgentsForWorkflow(workflow) {
334
336
  }
335
337
 
336
338
  async function deploy(tool, options) {
339
+ // Show tool recommendations
340
+ if (options.recommend) {
341
+ const cwd = process.cwd();
342
+
343
+ const spinner = ora('Analyzing project and detecting tools...').start();
344
+ let installedTools;
345
+ try {
346
+ installedTools = await ToolDetector.detectAll(cwd);
347
+ spinner.succeed('Analysis complete');
348
+ } catch {
349
+ installedTools = new Map();
350
+ spinner.warn('Could not detect tools');
351
+ }
352
+
353
+ // Determine workflow from latest session if available
354
+ let workflow = 'balanced';
355
+ const sessionPath = await findLatestSession(cwd);
356
+ if (sessionPath) {
357
+ workflow = await getFrameworkFromSession(sessionPath);
358
+ }
359
+
360
+ const recommender = new ToolRecommender({
361
+ workflow,
362
+ projectType: 'existing',
363
+ workflowContext: {},
364
+ installedTools
365
+ });
366
+
367
+ const recommendation = recommender.recommend();
368
+ console.log(ToolRecommender.formatForDisplay(recommendation));
369
+ return;
370
+ }
371
+
337
372
  // List tools
338
373
  if (options.list) {
339
374
  console.log(chalk.cyan.bold('\n📋 Available Deployment Tools:\n'));
@@ -0,0 +1,345 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const RunManager = require('../harness/run-manager');
6
+ const ContextWindowManager = require('../harness/context-window-manager');
7
+ const MilestoneTracker = require('../harness/milestone-tracker');
8
+ const FeatureManifestManager = require('../harness/feature-manifest');
9
+ const EventLogger = require('../harness/event-logger');
10
+ const ProviderBridge = require('../harness/provider-bridge');
11
+ const HeadlessAdapter = require('../harness/headless-adapter');
12
+ const { getEnvFilePath, loadEnvIntoProcess, configureAIProvider } = require('../ai/ai-config');
13
+
14
+ /**
15
+ * Harness command handlers
16
+ *
17
+ * Manages long-running AI agent sessions across context windows.
18
+ * Commands: start, resume, status, handoff, events, manifest
19
+ */
20
+
21
+ async function harnessStart(options) {
22
+ const cwd = process.cwd();
23
+
24
+ console.log(chalk.cyan.bold('\n🔗 Harness Engineering Protocol\n'));
25
+
26
+ // Load env
27
+ const envPath = getEnvFilePath(cwd);
28
+ if (await fs.pathExists(envPath)) {
29
+ loadEnvIntoProcess(envPath);
30
+ }
31
+
32
+ const manager = new RunManager(cwd);
33
+
34
+ // Check for existing active run
35
+ const existingRun = await manager.getCurrentRun();
36
+ if (existingRun && existingRun.status === 'running') {
37
+ console.log(chalk.yellow(`Active run found: ${existingRun.id}`));
38
+ console.log(chalk.yellow(`Status: ${existingRun.status} | Workflow: ${existingRun.workflow}\n`));
39
+ console.log(chalk.gray('Use "adf harness resume" to continue or "adf harness status" for details.\n'));
40
+ return;
41
+ }
42
+
43
+ // Determine workflow
44
+ const workflow = options.workflow || 'balanced';
45
+ const mode = options.headless ? 'headless' : 'interactive';
46
+
47
+ // Configure AI provider
48
+ let aiConfig = null;
49
+ if (!options.headless) {
50
+ aiConfig = await configureAIProvider(cwd);
51
+ }
52
+
53
+ // Create run
54
+ const run = await manager.createRun({
55
+ workflow,
56
+ mode,
57
+ spec: {
58
+ goals: options.goals ? options.goals.split(',') : [],
59
+ nonGoals: [],
60
+ constraints: [],
61
+ doneWhen: []
62
+ },
63
+ provider: aiConfig ? {
64
+ id: aiConfig.provider,
65
+ model: aiConfig.model,
66
+ contextWindowSize: null
67
+ } : { id: null, model: null, contextWindowSize: null }
68
+ });
69
+
70
+ await manager.startRun(run.id);
71
+
72
+ console.log(chalk.green(`\n✓ Harness run created: ${run.id}`));
73
+ console.log(chalk.gray(` Workflow: ${workflow}`));
74
+ console.log(chalk.gray(` Mode: ${mode}`));
75
+
76
+ // Open first context window
77
+ const cwManager = new ContextWindowManager(manager.getRunDir(run.id), run.id);
78
+ const window = await cwManager.openWindow();
79
+
80
+ console.log(chalk.gray(` Context Window: ${window.id} (sequence ${window.sequence})`));
81
+ console.log(chalk.cyan(`\nRun "adf init --harness --run-id ${run.id}" to start the interview.\n`));
82
+ }
83
+
84
+ async function harnessResume(options) {
85
+ const cwd = process.cwd();
86
+
87
+ console.log(chalk.cyan.bold('\n🔗 Resume Harness Run\n'));
88
+
89
+ const manager = new RunManager(cwd);
90
+
91
+ const runId = options.runId;
92
+ let run;
93
+
94
+ if (runId) {
95
+ run = await manager.loadRun(runId);
96
+ } else {
97
+ run = await manager.getCurrentRun();
98
+ }
99
+
100
+ if (!run) {
101
+ console.log(chalk.yellow('No harness run found to resume.\n'));
102
+ return;
103
+ }
104
+
105
+ if (run.status !== 'paused') {
106
+ console.log(chalk.yellow(`Run ${run.id} is not paused (status: ${run.status}).\n`));
107
+ return;
108
+ }
109
+
110
+ // Resume the run
111
+ await manager.resumeRun(run.id);
112
+
113
+ // Open new context window and consume handoff
114
+ const cwManager = new ContextWindowManager(manager.getRunDir(run.id), run.id);
115
+ const window = await cwManager.openWindow();
116
+ const handoff = await cwManager.consumeHandoffPackage('latest');
117
+
118
+ console.log(chalk.green(`✓ Run resumed: ${run.id}`));
119
+ console.log(chalk.gray(` Context Window: ${window.id} (sequence ${window.sequence})`));
120
+
121
+ if (handoff) {
122
+ console.log(chalk.cyan(`\n📋 Handoff Summary:`));
123
+ console.log(chalk.gray(` ${handoff.progressSummary || 'No summary'}`));
124
+ console.log(chalk.gray(` Completed: ${handoff.completedWork.length} items`));
125
+ console.log(chalk.gray(` Answered: ${handoff.position.answeredIds.length} questions`));
126
+ console.log(chalk.gray(` Remaining: ${handoff.position.remainingIds.length} questions`));
127
+ console.log(chalk.gray(` Knowledge: ${handoff.knowledgeGraph.totalItems} items`));
128
+ }
129
+
130
+ console.log(chalk.cyan(`\nRun "adf init --harness --run-id ${run.id}" to continue the interview.\n`));
131
+ }
132
+
133
+ async function harnessStatus(options) {
134
+ const cwd = process.cwd();
135
+
136
+ console.log(chalk.cyan.bold('\n🔗 Harness Status\n'));
137
+
138
+ const manager = new RunManager(cwd);
139
+
140
+ if (options.all) {
141
+ const runs = await manager.listRuns();
142
+ if (runs.length === 0) {
143
+ console.log(chalk.gray('No harness runs found.\n'));
144
+ return;
145
+ }
146
+
147
+ console.log(chalk.white(`Found ${runs.length} run(s):\n`));
148
+ for (const run of runs) {
149
+ const statusIcon = {
150
+ initializing: '🟡',
151
+ running: '🟢',
152
+ paused: '🟠',
153
+ completed: '✅',
154
+ failed: '❌'
155
+ }[run.status] || '⚪';
156
+
157
+ console.log(` ${statusIcon} ${run.id}`);
158
+ console.log(chalk.gray(` Status: ${run.status} | Workflow: ${run.workflow} | Mode: ${run.mode}`));
159
+ console.log(chalk.gray(` Created: ${new Date(run.createdAt).toLocaleString()}`));
160
+ console.log('');
161
+ }
162
+ return;
163
+ }
164
+
165
+ // Show current run
166
+ const run = await manager.getCurrentRun();
167
+ if (!run) {
168
+ console.log(chalk.gray('No active harness run.\n'));
169
+ console.log(chalk.gray('Use "adf harness start" to create one.\n'));
170
+ return;
171
+ }
172
+
173
+ console.log(chalk.white(`Run: ${run.id}`));
174
+ console.log(chalk.gray(`Status: ${run.status}`));
175
+ console.log(chalk.gray(`Workflow: ${run.workflow}`));
176
+ console.log(chalk.gray(`Mode: ${run.mode}`));
177
+ console.log(chalk.gray(`Provider: ${run.provider.id || 'not set'} / ${run.provider.model || 'not set'}`));
178
+ console.log(chalk.gray(`Session: ${run.sessionId || 'not linked'}`));
179
+ console.log(chalk.gray(`Context Windows: ${run.contextWindows.length}`));
180
+ console.log(chalk.gray(`Created: ${new Date(run.createdAt).toLocaleString()}`));
181
+ console.log(chalk.gray(`Updated: ${new Date(run.updatedAt).toLocaleString()}\n`));
182
+
183
+ // Show milestone progress if available
184
+ const milestoneTracker = new MilestoneTracker(manager.getRunDir(run.id));
185
+ if (await milestoneTracker.load()) {
186
+ const progress = milestoneTracker.getProgress();
187
+ console.log(chalk.cyan('Milestones:'));
188
+ console.log(chalk.gray(` Total: ${progress.total} | Completed: ${progress.completed} | Skipped: ${progress.skipped}`));
189
+ console.log(chalk.gray(` Progress: ${progress.percentage}%\n`));
190
+ }
191
+
192
+ // Show feature manifest progress if available
193
+ const manifestManager = new FeatureManifestManager(manager.getRunDir(run.id));
194
+ if (await manifestManager.load()) {
195
+ const progress = manifestManager.getProgress();
196
+ console.log(chalk.cyan('Features:'));
197
+ console.log(chalk.gray(` Total: ${progress.total} | Passed: ${progress.passed} | Remaining: ${progress.remaining}`));
198
+ console.log(chalk.gray(` Progress: ${progress.percentage}%\n`));
199
+ }
200
+ }
201
+
202
+ async function harnessHandoff(options) {
203
+ const cwd = process.cwd();
204
+
205
+ console.log(chalk.cyan.bold('\n🔗 Generate Handoff Package\n'));
206
+
207
+ const manager = new RunManager(cwd);
208
+ const run = await manager.getCurrentRun();
209
+
210
+ if (!run) {
211
+ console.log(chalk.yellow('No active harness run.\n'));
212
+ return;
213
+ }
214
+
215
+ const cwManager = new ContextWindowManager(manager.getRunDir(run.id), run.id);
216
+
217
+ if (options.consume) {
218
+ // Consume latest handoff
219
+ const handoff = await cwManager.consumeHandoffPackage('latest');
220
+ if (!handoff) {
221
+ console.log(chalk.yellow('No handoff package found.\n'));
222
+ return;
223
+ }
224
+
225
+ console.log(chalk.green('✓ Handoff package consumed:'));
226
+ console.log(chalk.gray(JSON.stringify(handoff.toJSON(), null, 2)));
227
+ return;
228
+ }
229
+
230
+ // Generate handoff from current state
231
+ // Load progress from linked session
232
+ const progressSummary = `Run ${run.id} - ${run.workflow} workflow`;
233
+
234
+ const handoff = await cwManager.generateHandoffPackage({
235
+ progressSummary,
236
+ completedWork: [],
237
+ nextSteps: [],
238
+ currentMilestone: { index: run.currentMilestoneIndex, title: '', percentage: 0 }
239
+ });
240
+
241
+ // Pause the run
242
+ await manager.pauseRun(run.id);
243
+ await cwManager.closeWindow('completed');
244
+
245
+ console.log(chalk.green(`✓ Handoff package generated`));
246
+ console.log(chalk.gray(` Source window: ${handoff.sourceWindowId}`));
247
+ console.log(chalk.gray(` Run paused. Resume with: adf harness resume\n`));
248
+ }
249
+
250
+ async function harnessEvents(options) {
251
+ const cwd = process.cwd();
252
+
253
+ console.log(chalk.cyan.bold('\n🔗 Harness Events\n'));
254
+
255
+ const manager = new RunManager(cwd);
256
+ const run = await manager.getCurrentRun();
257
+
258
+ if (!run) {
259
+ console.log(chalk.yellow('No active harness run.\n'));
260
+ return;
261
+ }
262
+
263
+ const logger = manager.getEventLogger(run.id);
264
+
265
+ if (options.stats) {
266
+ const stats = await logger.getStats();
267
+ console.log(chalk.white(`Total events: ${stats.total}\n`));
268
+ for (const [type, count] of Object.entries(stats.byType)) {
269
+ console.log(chalk.gray(` ${type}: ${count}`));
270
+ }
271
+ console.log('');
272
+ return;
273
+ }
274
+
275
+ const count = parseInt(options.tail) || 20;
276
+ const events = await logger.tail(count);
277
+
278
+ if (events.length === 0) {
279
+ console.log(chalk.gray('No events recorded.\n'));
280
+ return;
281
+ }
282
+
283
+ console.log(chalk.white(`Last ${events.length} events:\n`));
284
+ for (const event of events) {
285
+ const time = new Date(event.timestamp).toLocaleTimeString();
286
+ const typeColor = {
287
+ answer: chalk.green,
288
+ skip: chalk.yellow,
289
+ error: chalk.red,
290
+ handoff: chalk.cyan,
291
+ checkpoint: chalk.gray,
292
+ window_open: chalk.blue,
293
+ window_close: chalk.blue
294
+ }[event.type] || chalk.white;
295
+
296
+ console.log(` ${chalk.gray(time)} ${typeColor(event.type.padEnd(18))} ${chalk.gray(JSON.stringify(event.data).slice(0, 80))}`);
297
+ }
298
+ console.log('');
299
+ }
300
+
301
+ async function harnessManifest(options) {
302
+ const cwd = process.cwd();
303
+
304
+ console.log(chalk.cyan.bold('\n🔗 Feature Manifest\n'));
305
+
306
+ const manager = new RunManager(cwd);
307
+ const run = await manager.getCurrentRun();
308
+
309
+ if (!run) {
310
+ console.log(chalk.yellow('No active harness run.\n'));
311
+ return;
312
+ }
313
+
314
+ const manifestManager = new FeatureManifestManager(manager.getRunDir(run.id));
315
+
316
+ if (!await manifestManager.load()) {
317
+ console.log(chalk.gray('No feature manifest generated yet.\n'));
318
+ console.log(chalk.gray('Complete an interview session to generate features.\n'));
319
+ return;
320
+ }
321
+
322
+ const manifest = manifestManager.getManifest();
323
+ const progress = manifestManager.getProgress();
324
+
325
+ console.log(chalk.white(`Features: ${progress.total} total | ${progress.passed} passed | ${progress.remaining} remaining`));
326
+ console.log(chalk.white(`Progress: ${progress.percentage}%\n`));
327
+
328
+ for (const feature of manifest.features) {
329
+ const icon = feature.passes ? chalk.green('✓') : chalk.gray('○');
330
+ console.log(` ${icon} ${feature.title}`);
331
+ if (feature.description) {
332
+ console.log(chalk.gray(` ${feature.description.slice(0, 80)}`));
333
+ }
334
+ }
335
+ console.log('');
336
+ }
337
+
338
+ module.exports = {
339
+ harnessStart,
340
+ harnessResume,
341
+ harnessStatus,
342
+ harnessHandoff,
343
+ harnessEvents,
344
+ harnessManifest
345
+ };
@@ -9,6 +9,9 @@ const {
9
9
  } = require('../utils/project-detector');
10
10
  const FrameworkDetector = require('../utils/framework-detector');
11
11
  const ContextExtractor = require('../utils/context-extractor');
12
+ const ToolDetector = require('../utils/tool-detector');
13
+ const ToolRecommender = require('../utils/tool-recommender');
14
+ const ToolFeatureRegistry = require('../utils/tool-feature-registry');
12
15
  const SynthesisEngine = require('../analysis/synthesis-engine');
13
16
  const HeuristicGapAnalyzer = require('../analysis/heuristic-gap-analyzer');
14
17
  const AIGapAnalyzer = require('../analysis/ai-gap-analyzer');
@@ -38,7 +41,8 @@ async function init(options) {
38
41
  const frameworkNames = {
39
42
  'agent-native': 'Agent-Native',
40
43
  'openspec': 'OpenSpec',
41
- 'specification-driven': 'Specification-Driven'
44
+ 'specification-driven': 'Specification-Driven',
45
+ 'gemini-conductor': 'Gemini CLI Conductor'
42
46
  };
43
47
 
44
48
  const displayNames = detectedFrameworks
@@ -371,6 +375,7 @@ async function init(options) {
371
375
 
372
376
  // Determine workflow/framework
373
377
  let workflow;
378
+ let workflowContext = {};
374
379
 
375
380
  if (options.rapid) {
376
381
  workflow = 'rapid';
@@ -382,8 +387,14 @@ async function init(options) {
382
387
  workflow = 'comprehensive';
383
388
  console.log(chalk.blue('\nUsing: Level 3: Comprehensive (Agent-Native) - from --comprehensive flag'));
384
389
  } else {
385
- // Interactive workflow selection
386
- workflow = await getWorkflowRecommendation(projectType);
390
+ // Interactive workflow selection (returns { workflow, context } or plain string for backward compat)
391
+ const workflowResult = await getWorkflowRecommendation(projectType);
392
+ if (typeof workflowResult === 'string') {
393
+ workflow = workflowResult;
394
+ } else {
395
+ workflow = workflowResult.workflow;
396
+ workflowContext = workflowResult.context || {};
397
+ }
387
398
  }
388
399
 
389
400
  // Create .adf directory
@@ -392,6 +403,74 @@ async function init(options) {
392
403
  // AI already configured above - pass to interviewer
393
404
  // Start interview
394
405
  const interviewer = new Interviewer(workflow, cwd, null, aiConfig);
406
+
407
+ // Harness integration (opt-in via --harness flag)
408
+ if (options.harness) {
409
+ const RunManager = require('../harness/run-manager');
410
+ const ContextWindowManager = require('../harness/context-window-manager');
411
+ const MilestoneTracker = require('../harness/milestone-tracker');
412
+ const EventLogger = require('../harness/event-logger');
413
+
414
+ const runManager = new RunManager(cwd);
415
+ let run;
416
+
417
+ if (options.runId) {
418
+ // Resume existing harness run
419
+ run = await runManager.loadRun(options.runId);
420
+ if (!run) {
421
+ console.log(chalk.red(`Harness run not found: ${options.runId}\n`));
422
+ return;
423
+ }
424
+ } else {
425
+ // Create new harness run
426
+ run = await runManager.createRun({
427
+ workflow,
428
+ mode: options.headless ? 'headless' : 'interactive',
429
+ provider: aiConfig ? {
430
+ id: aiConfig.provider,
431
+ model: aiConfig.model,
432
+ contextWindowSize: null
433
+ } : { id: null, model: null, contextWindowSize: null }
434
+ });
435
+ await runManager.startRun(run.id);
436
+ }
437
+
438
+ const runDir = runManager.getRunDir(run.id);
439
+ const cwManager = new ContextWindowManager(runDir, run.id);
440
+ const eventLogger = EventLogger.forRun(runDir);
441
+ const milestoneTracker = new MilestoneTracker(runDir);
442
+
443
+ // Open context window if not already active
444
+ let currentWindow = cwManager.getCurrentWindow();
445
+ if (!currentWindow) {
446
+ currentWindow = await cwManager.openWindow();
447
+ }
448
+
449
+ // Consume handoff if resuming
450
+ if (options.runId && run.status === 'running') {
451
+ const handoff = await cwManager.consumeHandoffPackage('latest');
452
+ if (handoff) {
453
+ interviewer.consumeHandoff(handoff);
454
+ console.log(chalk.green(`✓ Handoff consumed from window ${handoff.sourceWindowId}\n`));
455
+ }
456
+ }
457
+
458
+ // Link session to run
459
+ run.sessionId = interviewer.sessionId;
460
+ await runManager.saveRun(run);
461
+
462
+ // Set harness on interviewer
463
+ interviewer.setHarness({
464
+ run,
465
+ runManager,
466
+ contextWindowManager: cwManager,
467
+ eventLogger,
468
+ milestoneTracker
469
+ });
470
+
471
+ console.log(chalk.cyan(`🔗 Harness: ${run.id} | Window: ${currentWindow.id}\n`));
472
+ }
473
+
395
474
  const sessionPath = await interviewer.start();
396
475
 
397
476
  // Show completion message
@@ -423,18 +502,64 @@ async function init(options) {
423
502
  ]);
424
503
 
425
504
  if (deployNow) {
505
+ // Detect installed tools and generate recommendations
506
+ const detectSpinner = ora('Detecting installed tools...').start();
507
+ let installedTools;
508
+ try {
509
+ installedTools = await ToolDetector.detectAll(cwd);
510
+ const installedCount = [...installedTools.values()].filter(t => t.installed).length;
511
+ detectSpinner.succeed(`Detected ${installedCount} installed tool(s)`);
512
+ } catch {
513
+ installedTools = new Map();
514
+ detectSpinner.warn('Could not detect tools, showing all options');
515
+ }
516
+
517
+ const recommender = new ToolRecommender({
518
+ workflow,
519
+ projectType: projectType.type,
520
+ workflowContext,
521
+ installedTools
522
+ });
523
+ const recommendation = recommender.recommend();
524
+
525
+ // Display formatted recommendations
526
+ console.log(ToolRecommender.formatForDisplay(recommendation));
527
+
528
+ // Build pre-selected set from primary + complementary
529
+ const preSelected = new Set();
530
+ if (recommendation.primary) preSelected.add(recommendation.primary.toolId);
531
+ for (const c of recommendation.complementary) preSelected.add(c.toolId);
532
+
533
+ // Build choices for all deployable tools (exclude a2a protocol)
534
+ const allToolIds = Object.keys(ToolFeatureRegistry.REGISTRY).filter(id => {
535
+ const cfg = ToolFeatureRegistry.getDetectionConfig(id);
536
+ return cfg && cfg.category !== 'protocol';
537
+ });
538
+
539
+ const choices = allToolIds.map(id => {
540
+ const reg = ToolFeatureRegistry.REGISTRY[id];
541
+ const scored = recommendation.ranked.find(r => r.toolId === id);
542
+ const installStatus = installedTools.get(id);
543
+ const installed = installStatus && installStatus.installed;
544
+
545
+ let label = reg.name;
546
+ if (scored) label += chalk.gray(` (${scored.totalScore}pts)`);
547
+ if (installed) label += chalk.green(' *');
548
+ if (preSelected.has(id)) label += chalk.yellow(' (recommended)');
549
+
550
+ return {
551
+ name: label,
552
+ value: id,
553
+ checked: preSelected.has(id)
554
+ };
555
+ });
556
+
426
557
  const { tools } = await inquirer.prompt([
427
558
  {
428
559
  type: 'checkbox',
429
560
  name: 'tools',
430
561
  message: 'Select tools (space to select, enter to confirm):',
431
- choices: [
432
- { name: 'Windsurf', value: 'windsurf' },
433
- { name: 'Cursor', value: 'cursor' },
434
- { name: 'VSCode/Copilot', value: 'vscode' },
435
- { name: 'Claude Code', value: 'claude-code' },
436
- { name: 'Gemini CLI', value: 'gemini-cli' }
437
- ],
562
+ choices,
438
563
  validate: (answer) => {
439
564
  if (answer.length === 0) {
440
565
  return 'You must choose at least one tool.';