claude-code-workflow 6.3.27 → 6.3.29

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 (133) hide show
  1. package/.claude/CLAUDE.md +7 -1
  2. package/.claude/agents/action-planning-agent.md +1 -0
  3. package/.claude/agents/cli-discuss-agent.md +391 -0
  4. package/.claude/agents/cli-execution-agent.md +2 -0
  5. package/.claude/agents/cli-explore-agent.md +2 -1
  6. package/.claude/agents/cli-lite-planning-agent.md +1 -0
  7. package/.claude/agents/cli-planning-agent.md +1 -0
  8. package/.claude/agents/code-developer.md +1 -0
  9. package/.claude/agents/conceptual-planning-agent.md +2 -0
  10. package/.claude/agents/context-search-agent.md +1 -0
  11. package/.claude/agents/debug-explore-agent.md +2 -0
  12. package/.claude/agents/doc-generator.md +1 -0
  13. package/.claude/agents/issue-plan-agent.md +2 -1
  14. package/.claude/agents/issue-queue-agent.md +2 -1
  15. package/.claude/agents/memory-bridge.md +2 -0
  16. package/.claude/agents/test-context-search-agent.md +2 -0
  17. package/.claude/agents/test-fix-agent.md +1 -0
  18. package/.claude/agents/ui-design-agent.md +2 -0
  19. package/.claude/agents/universal-executor.md +1 -0
  20. package/.claude/commands/issue/execute.md +269 -176
  21. package/.claude/commands/workflow/debug.md +6 -0
  22. package/.claude/commands/workflow/execute.md +4 -0
  23. package/.claude/commands/workflow/lite-execute.md +4 -0
  24. package/.claude/commands/workflow/lite-lite-lite.md +433 -0
  25. package/.claude/commands/workflow/multi-cli-plan.md +510 -0
  26. package/.claude/commands/workflow/review-fix.md +4 -0
  27. package/.claude/commands/workflow/test-cycle-execute.md +4 -0
  28. package/.claude/skills/ccw/SKILL.md +262 -372
  29. package/.claude/skills/ccw/command.json +547 -0
  30. package/.claude/skills/ccw-help/SKILL.md +46 -107
  31. package/.claude/skills/ccw-help/command.json +511 -0
  32. package/.claude/skills/skill-tuning/SKILL.md +303 -0
  33. package/.claude/skills/skill-tuning/phases/actions/action-abort.md +164 -0
  34. package/.claude/skills/skill-tuning/phases/actions/action-analyze-requirements.md +406 -0
  35. package/.claude/skills/skill-tuning/phases/actions/action-apply-fix.md +206 -0
  36. package/.claude/skills/skill-tuning/phases/actions/action-complete.md +195 -0
  37. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-agent.md +317 -0
  38. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-context.md +243 -0
  39. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-dataflow.md +318 -0
  40. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-docs.md +299 -0
  41. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-memory.md +269 -0
  42. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-token-consumption.md +200 -0
  43. package/.claude/skills/skill-tuning/phases/actions/action-gemini-analysis.md +322 -0
  44. package/.claude/skills/skill-tuning/phases/actions/action-generate-report.md +228 -0
  45. package/.claude/skills/skill-tuning/phases/actions/action-init.md +149 -0
  46. package/.claude/skills/skill-tuning/phases/actions/action-propose-fixes.md +317 -0
  47. package/.claude/skills/skill-tuning/phases/actions/action-verify.md +222 -0
  48. package/.claude/skills/skill-tuning/phases/orchestrator.md +377 -0
  49. package/.claude/skills/skill-tuning/phases/state-schema.md +378 -0
  50. package/.claude/skills/skill-tuning/specs/category-mappings.json +284 -0
  51. package/.claude/skills/skill-tuning/specs/dimension-mapping.md +212 -0
  52. package/.claude/skills/skill-tuning/specs/problem-taxonomy.md +318 -0
  53. package/.claude/skills/skill-tuning/specs/quality-gates.md +263 -0
  54. package/.claude/skills/skill-tuning/specs/skill-authoring-principles.md +189 -0
  55. package/.claude/skills/skill-tuning/specs/tuning-strategies.md +1537 -0
  56. package/.claude/skills/skill-tuning/templates/diagnosis-report.md +153 -0
  57. package/.claude/skills/skill-tuning/templates/fix-proposal.md +204 -0
  58. package/.claude/workflows/cli-templates/schemas/multi-cli-discussion-schema.json +421 -0
  59. package/.claude/workflows/cli-tools-usage.md +0 -41
  60. package/.codex/prompts/issue-execute.md +72 -12
  61. package/README.md +3 -0
  62. package/ccw/dist/core/data-aggregator.d.ts +2 -0
  63. package/ccw/dist/core/data-aggregator.d.ts.map +1 -1
  64. package/ccw/dist/core/data-aggregator.js +5 -2
  65. package/ccw/dist/core/data-aggregator.js.map +1 -1
  66. package/ccw/dist/core/lite-scanner.d.ts +2 -1
  67. package/ccw/dist/core/lite-scanner.d.ts.map +1 -1
  68. package/ccw/dist/core/lite-scanner.js +348 -6
  69. package/ccw/dist/core/lite-scanner.js.map +1 -1
  70. package/ccw/dist/core/routes/session-routes.d.ts.map +1 -1
  71. package/ccw/dist/core/routes/session-routes.js +166 -48
  72. package/ccw/dist/core/routes/session-routes.js.map +1 -1
  73. package/ccw/dist/core/routes/system-routes.d.ts.map +1 -1
  74. package/ccw/dist/core/routes/system-routes.js +87 -0
  75. package/ccw/dist/core/routes/system-routes.js.map +1 -1
  76. package/ccw/dist/core/server.js +2 -2
  77. package/ccw/dist/core/server.js.map +1 -1
  78. package/ccw/dist/tools/memory-update-queue.d.ts.map +1 -1
  79. package/ccw/dist/tools/memory-update-queue.js +5 -11
  80. package/ccw/dist/tools/memory-update-queue.js.map +1 -1
  81. package/ccw/scripts/IMPLEMENTATION-SUMMARY.md +226 -0
  82. package/ccw/scripts/QUICK-REFERENCE.md +135 -0
  83. package/ccw/scripts/README-memory-embedder.md +157 -0
  84. package/ccw/scripts/__pycache__/memory_embedder.cpython-313.pyc +0 -0
  85. package/ccw/scripts/__pycache__/test_memory_embedder.cpython-313-pytest-8.4.2.pyc +0 -0
  86. package/ccw/scripts/memory-embedder-example.ts +184 -0
  87. package/ccw/scripts/memory_embedder.py +428 -0
  88. package/ccw/scripts/test_memory_embedder.py +245 -0
  89. package/ccw/src/core/data-aggregator.ts +7 -2
  90. package/ccw/src/core/lite-scanner.ts +495 -6
  91. package/ccw/src/core/routes/session-routes.ts +201 -48
  92. package/ccw/src/core/routes/system-routes.ts +102 -0
  93. package/ccw/src/core/server.ts +2 -2
  94. package/ccw/src/templates/dashboard-css/01-base.css +8 -0
  95. package/ccw/src/templates/dashboard-css/02-session.css +81 -0
  96. package/ccw/src/templates/dashboard-css/03-tasks.css +5 -0
  97. package/ccw/src/templates/dashboard-css/04-lite-tasks.css +3071 -0
  98. package/ccw/src/templates/dashboard-css/21-cli-toolmgmt.css +157 -0
  99. package/ccw/src/templates/dashboard-css/32-issue-manager.css +23 -0
  100. package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +38 -4
  101. package/ccw/src/templates/dashboard-js/components/hook-manager.js +68 -34
  102. package/ccw/src/templates/dashboard-js/components/navigation.js +24 -4
  103. package/ccw/src/templates/dashboard-js/i18n.js +278 -4
  104. package/ccw/src/templates/dashboard-js/views/api-settings.js +32 -0
  105. package/ccw/src/templates/dashboard-js/views/claude-manager.js +44 -3
  106. package/ccw/src/templates/dashboard-js/views/cli-manager.js +303 -31
  107. package/ccw/src/templates/dashboard-js/views/history.js +44 -6
  108. package/ccw/src/templates/dashboard-js/views/home.js +1 -0
  109. package/ccw/src/templates/dashboard-js/views/issue-manager.js +54 -7
  110. package/ccw/src/templates/dashboard-js/views/lite-tasks.js +2621 -4
  111. package/ccw/src/templates/dashboard.html +5 -0
  112. package/ccw/src/tools/memory-update-queue.js +5 -11
  113. package/package.json +2 -1
  114. package/.claude/skills/ccw/index/command-capabilities.json +0 -127
  115. package/.claude/skills/ccw/index/intent-rules.json +0 -136
  116. package/.claude/skills/ccw/index/workflow-chains.json +0 -451
  117. package/.claude/skills/ccw/phases/actions/bugfix.md +0 -218
  118. package/.claude/skills/ccw/phases/actions/coupled.md +0 -194
  119. package/.claude/skills/ccw/phases/actions/docs.md +0 -93
  120. package/.claude/skills/ccw/phases/actions/full.md +0 -154
  121. package/.claude/skills/ccw/phases/actions/issue.md +0 -201
  122. package/.claude/skills/ccw/phases/actions/rapid.md +0 -104
  123. package/.claude/skills/ccw/phases/actions/review-fix.md +0 -84
  124. package/.claude/skills/ccw/phases/actions/tdd.md +0 -66
  125. package/.claude/skills/ccw/phases/actions/ui.md +0 -79
  126. package/.claude/skills/ccw/phases/orchestrator.md +0 -435
  127. package/.claude/skills/ccw/specs/intent-classification.md +0 -336
  128. package/.claude/skills/ccw-help/index/all-agents.json +0 -82
  129. package/.claude/skills/ccw-help/index/all-commands.json +0 -882
  130. package/.claude/skills/ccw-help/index/by-category.json +0 -914
  131. package/.claude/skills/ccw-help/index/by-use-case.json +0 -896
  132. package/.claude/skills/ccw-help/index/command-relationships.json +0 -160
  133. package/.claude/skills/ccw-help/index/essential-commands.json +0 -112
@@ -77,6 +77,7 @@ interface DashboardData {
77
77
  liteTasks: {
78
78
  litePlan: unknown[];
79
79
  liteFix: unknown[];
80
+ multiCliPlan: unknown[];
80
81
  };
81
82
  reviewData: ReviewData | null;
82
83
  projectOverview: ProjectOverview | null;
@@ -88,6 +89,7 @@ interface DashboardData {
88
89
  reviewFindings: number;
89
90
  litePlanCount: number;
90
91
  liteFixCount: number;
92
+ multiCliPlanCount: number;
91
93
  };
92
94
  }
93
95
 
@@ -211,7 +213,8 @@ export async function aggregateData(sessions: ScanSessionsResult, workflowDir: s
211
213
  archivedSessions: [],
212
214
  liteTasks: {
213
215
  litePlan: [],
214
- liteFix: []
216
+ liteFix: [],
217
+ multiCliPlan: []
215
218
  },
216
219
  reviewData: null,
217
220
  projectOverview: null,
@@ -222,7 +225,8 @@ export async function aggregateData(sessions: ScanSessionsResult, workflowDir: s
222
225
  completedTasks: 0,
223
226
  reviewFindings: 0,
224
227
  litePlanCount: 0,
225
- liteFixCount: 0
228
+ liteFixCount: 0,
229
+ multiCliPlanCount: 0
226
230
  }
227
231
  };
228
232
 
@@ -257,6 +261,7 @@ export async function aggregateData(sessions: ScanSessionsResult, workflowDir: s
257
261
  data.liteTasks = liteTasks;
258
262
  data.statistics.litePlanCount = liteTasks.litePlan.length;
259
263
  data.statistics.liteFixCount = liteTasks.liteFix.length;
264
+ data.statistics.multiCliPlanCount = liteTasks.multiCliPlan.length;
260
265
  } catch (err) {
261
266
  console.error('Error scanning lite tasks:', (err as Error).message);
262
267
  }
@@ -60,9 +60,47 @@ interface LiteSession {
60
60
  progress: Progress;
61
61
  }
62
62
 
63
+ // Multi-CLI specific session state from session-state.json
64
+ interface MultiCliSessionState {
65
+ session_id: string;
66
+ task_description: string;
67
+ status: string;
68
+ current_phase: number;
69
+ phases: Record<string, { status: string; rounds_completed?: number }>;
70
+ ace_context?: { relevant_files: string[]; detected_patterns: string[] };
71
+ user_decisions?: Array<{ round: number; decision: string; selected: string }>;
72
+ updated_at?: string;
73
+ }
74
+
75
+ // Discussion topic structure for frontend rendering
76
+ interface DiscussionTopic {
77
+ title: string;
78
+ description: string;
79
+ scope: { included: string[]; excluded: string[] };
80
+ keyQuestions: string[];
81
+ status: string;
82
+ tags: string[];
83
+ }
84
+
85
+ // Extended session interface for multi-cli-plan
86
+ interface MultiCliSession extends LiteSession {
87
+ roundCount: number;
88
+ topicTitle: string;
89
+ status: string;
90
+ metadata: {
91
+ roundId: number;
92
+ timestamp: string;
93
+ currentPhase: number;
94
+ };
95
+ discussionTopic: DiscussionTopic;
96
+ rounds: RoundSynthesis[];
97
+ latestSynthesis: RoundSynthesis | null;
98
+ }
99
+
63
100
  interface LiteTasks {
64
101
  litePlan: LiteSession[];
65
102
  liteFix: LiteSession[];
103
+ multiCliPlan: LiteSession[];
66
104
  }
67
105
 
68
106
  interface LiteTaskDetail {
@@ -84,13 +122,15 @@ interface LiteTaskDetail {
84
122
  export async function scanLiteTasks(workflowDir: string): Promise<LiteTasks> {
85
123
  const litePlanDir = join(workflowDir, '.lite-plan');
86
124
  const liteFixDir = join(workflowDir, '.lite-fix');
125
+ const multiCliDir = join(workflowDir, '.multi-cli-plan');
87
126
 
88
- const [litePlan, liteFix] = await Promise.all([
127
+ const [litePlan, liteFix, multiCliPlan] = await Promise.all([
89
128
  scanLiteDir(litePlanDir, 'lite-plan'),
90
129
  scanLiteDir(liteFixDir, 'lite-fix'),
130
+ scanMultiCliDir(multiCliDir),
91
131
  ]);
92
132
 
93
- return { litePlan, liteFix };
133
+ return { litePlan, liteFix, multiCliPlan };
94
134
  }
95
135
 
96
136
  /**
@@ -142,6 +182,427 @@ async function scanLiteDir(dir: string, type: string): Promise<LiteSession[]> {
142
182
  }
143
183
  }
144
184
 
185
+ /**
186
+ * Load session-state.json from multi-cli session directory
187
+ * @param sessionPath - Session directory path
188
+ * @returns Session state or null if not found
189
+ */
190
+ async function loadSessionState(sessionPath: string): Promise<MultiCliSessionState | null> {
191
+ const statePath = join(sessionPath, 'session-state.json');
192
+ try {
193
+ const content = await readFile(statePath, 'utf8');
194
+ return JSON.parse(content);
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Build discussion topic structure from session state and synthesis
202
+ * @param state - Session state from session-state.json
203
+ * @param synthesis - Latest round synthesis
204
+ * @returns Discussion topic for frontend rendering
205
+ */
206
+ function buildDiscussionTopic(
207
+ state: MultiCliSessionState | null,
208
+ synthesis: RoundSynthesis | null
209
+ ): DiscussionTopic {
210
+ const keyQuestions = synthesis?.clarification_questions || [];
211
+ const solutions = synthesis?.solutions || [];
212
+
213
+ return {
214
+ title: state?.task_description || 'Discussion Topic',
215
+ description: solutions[0]?.summary || '',
216
+ scope: {
217
+ included: state?.ace_context?.relevant_files || [],
218
+ excluded: [],
219
+ },
220
+ keyQuestions,
221
+ status: state?.status || 'analyzing',
222
+ tags: solutions.map((s) => s.name).slice(0, 3),
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Scan multi-cli-plan directory for sessions
228
+ * @param dir - Directory path to .multi-cli-plan
229
+ * @returns Array of multi-cli sessions with extended metadata
230
+ */
231
+ async function scanMultiCliDir(dir: string): Promise<MultiCliSession[]> {
232
+ try {
233
+ const entries = await readdir(dir, { withFileTypes: true });
234
+
235
+ const sessions = (await Promise.all(
236
+ entries
237
+ .filter((entry) => entry.isDirectory())
238
+ .map(async (entry) => {
239
+ const sessionPath = join(dir, entry.name);
240
+
241
+ const [createdAt, syntheses, sessionState, planJson] = await Promise.all([
242
+ getCreatedTime(sessionPath),
243
+ loadRoundSyntheses(sessionPath),
244
+ loadSessionState(sessionPath),
245
+ loadPlanJson(sessionPath),
246
+ ]);
247
+
248
+ // Extract data from syntheses
249
+ const roundCount = syntheses.length;
250
+ const latestSynthesis = syntheses.length > 0 ? syntheses[syntheses.length - 1] : null;
251
+
252
+ // Calculate progress based on round count and convergence
253
+ const progress = calculateMultiCliProgress(syntheses);
254
+
255
+ // Build discussion topic for frontend
256
+ const discussionTopic = buildDiscussionTopic(sessionState, latestSynthesis);
257
+
258
+ // Determine status from session state or synthesis convergence
259
+ const status = sessionState?.status ||
260
+ (latestSynthesis?.convergence?.recommendation === 'converged' ? 'converged' : 'analyzing');
261
+
262
+ // Use plan.json if available, otherwise extract from synthesis
263
+ const plan = planJson || latestSynthesis;
264
+ // Use tasks from plan.json if available, otherwise extract from synthesis
265
+ const tasks = (planJson as any)?.tasks?.length > 0
266
+ ? normalizePlanJsonTasks((planJson as any).tasks)
267
+ : extractTasksFromSyntheses(syntheses);
268
+
269
+ const session: MultiCliSession = {
270
+ id: entry.name,
271
+ type: 'multi-cli-plan',
272
+ path: sessionPath,
273
+ createdAt,
274
+ plan,
275
+ tasks,
276
+ progress,
277
+ // Extended multi-cli specific fields
278
+ roundCount,
279
+ topicTitle: sessionState?.task_description || 'Discussion Topic',
280
+ status,
281
+ metadata: {
282
+ roundId: roundCount,
283
+ timestamp: sessionState?.updated_at || createdAt,
284
+ currentPhase: sessionState?.current_phase || 1,
285
+ },
286
+ discussionTopic,
287
+ rounds: syntheses,
288
+ latestSynthesis,
289
+ };
290
+
291
+ return session;
292
+ }),
293
+ ))
294
+ .filter((session): session is MultiCliSession => session !== null)
295
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
296
+
297
+ return sessions;
298
+ } catch (err: any) {
299
+ if (err?.code === 'ENOENT') return [];
300
+ console.error(`Error scanning ${dir}:`, err?.message || String(err));
301
+ return [];
302
+ }
303
+ }
304
+
305
+ // NEW Schema types for multi-cli synthesis
306
+ interface SolutionFileAction {
307
+ file: string;
308
+ line: number;
309
+ action: 'modify' | 'create' | 'delete';
310
+ }
311
+
312
+ interface SolutionTask {
313
+ id: string;
314
+ name: string;
315
+ depends_on: string[];
316
+ files: SolutionFileAction[];
317
+ key_point: string | null;
318
+ }
319
+
320
+ interface SolutionImplementationPlan {
321
+ approach: string;
322
+ tasks: SolutionTask[];
323
+ execution_flow: string;
324
+ milestones: string[];
325
+ }
326
+
327
+ interface SolutionDependencies {
328
+ internal: string[];
329
+ external: string[];
330
+ }
331
+
332
+ interface Solution {
333
+ name: string;
334
+ source_cli: string[];
335
+ feasibility: number; // 0-1
336
+ effort: 'low' | 'medium' | 'high';
337
+ risk: 'low' | 'medium' | 'high';
338
+ summary: string;
339
+ implementation_plan: SolutionImplementationPlan;
340
+ dependencies: SolutionDependencies;
341
+ technical_concerns: string[];
342
+ }
343
+
344
+ interface SynthesisConvergence {
345
+ score: number;
346
+ new_insights: boolean;
347
+ recommendation: 'converged' | 'continue' | 'user_input_needed';
348
+ }
349
+
350
+ interface SynthesisCrossVerification {
351
+ agreements: string[];
352
+ disagreements: string[];
353
+ resolution: string;
354
+ }
355
+
356
+ interface RoundSynthesis {
357
+ round: number;
358
+ // NEW schema fields
359
+ solutions?: Solution[];
360
+ convergence?: SynthesisConvergence;
361
+ cross_verification?: SynthesisCrossVerification;
362
+ clarification_questions?: string[];
363
+ // OLD schema fields (backward compatibility)
364
+ converged?: boolean;
365
+ tasks?: unknown[];
366
+ synthesis?: unknown;
367
+ [key: string]: unknown;
368
+ }
369
+
370
+ /**
371
+ * Load all synthesis.json files from rounds subdirectories
372
+ * @param sessionPath - Session directory path
373
+ * @returns Array of synthesis objects sorted by round number
374
+ */
375
+ async function loadRoundSyntheses(sessionPath: string): Promise<RoundSynthesis[]> {
376
+ const roundsDir = join(sessionPath, 'rounds');
377
+ const syntheses: RoundSynthesis[] = [];
378
+
379
+ try {
380
+ const roundEntries = await readdir(roundsDir, { withFileTypes: true });
381
+
382
+ const roundDirs = roundEntries
383
+ .filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name))
384
+ .map((entry) => ({
385
+ name: entry.name,
386
+ num: parseInt(entry.name, 10),
387
+ }))
388
+ .sort((a, b) => a.num - b.num);
389
+
390
+ for (const roundDir of roundDirs) {
391
+ const synthesisPath = join(roundsDir, roundDir.name, 'synthesis.json');
392
+ try {
393
+ const content = await readFile(synthesisPath, 'utf8');
394
+ const synthesis = JSON.parse(content) as RoundSynthesis;
395
+ synthesis.round = roundDir.num;
396
+ syntheses.push(synthesis);
397
+ } catch (e) {
398
+ console.warn('Failed to parse synthesis file:', synthesisPath, (e as Error).message);
399
+ }
400
+ }
401
+ } catch (e) {
402
+ // Ignore ENOENT errors (directory doesn't exist), warn on others
403
+ if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
404
+ console.warn('Failed to read rounds directory:', roundsDir, (e as Error).message);
405
+ }
406
+ }
407
+
408
+ return syntheses;
409
+ }
410
+
411
+ // Extended Progress interface for multi-cli sessions
412
+ interface MultiCliProgress extends Progress {
413
+ convergenceScore?: number;
414
+ recommendation?: 'converged' | 'continue' | 'user_input_needed';
415
+ solutionsCount?: number;
416
+ avgFeasibility?: number;
417
+ }
418
+
419
+ /**
420
+ * Calculate progress for multi-cli-plan sessions
421
+ * Uses new convergence.score and convergence.recommendation when available
422
+ * Falls back to old converged boolean for backward compatibility
423
+ * @param syntheses - Array of round syntheses
424
+ * @returns Progress info with convergence metrics
425
+ */
426
+ function calculateMultiCliProgress(syntheses: RoundSynthesis[]): MultiCliProgress {
427
+ if (syntheses.length === 0) {
428
+ return { total: 0, completed: 0, percentage: 0 };
429
+ }
430
+
431
+ const latestSynthesis = syntheses[syntheses.length - 1];
432
+
433
+ // NEW schema: Use convergence object
434
+ if (latestSynthesis.convergence) {
435
+ const { score, recommendation } = latestSynthesis.convergence;
436
+ const isConverged = recommendation === 'converged';
437
+
438
+ // Calculate solutions metrics
439
+ const solutions = latestSynthesis.solutions || [];
440
+ const solutionsCount = solutions.length;
441
+ const avgFeasibility = solutionsCount > 0
442
+ ? solutions.reduce((sum, s) => sum + (s.feasibility || 0), 0) / solutionsCount
443
+ : 0;
444
+
445
+ // Total is based on rounds, percentage derived from convergence score
446
+ const total = syntheses.length;
447
+ const completed = isConverged ? total : Math.max(0, total - 1);
448
+ const percentage = isConverged ? 100 : Math.round(score * 100);
449
+
450
+ return {
451
+ total,
452
+ completed,
453
+ percentage,
454
+ convergenceScore: score,
455
+ recommendation,
456
+ solutionsCount,
457
+ avgFeasibility: Math.round(avgFeasibility * 100) / 100
458
+ };
459
+ }
460
+
461
+ // OLD schema: Fallback to converged boolean
462
+ const isConverged = latestSynthesis.converged === true;
463
+ const total = syntheses.length;
464
+ const completed = isConverged ? total : Math.max(0, total - 1);
465
+ const percentage = isConverged ? 100 : Math.round((completed / Math.max(total, 1)) * 100);
466
+
467
+ return { total, completed, percentage };
468
+ }
469
+
470
+ /**
471
+ * Extract tasks from synthesis objects
472
+ * NEW schema: Extract from solutions[].implementation_plan.tasks
473
+ * OLD schema: Extract from tasks[] array directly
474
+ * @param syntheses - Array of round syntheses
475
+ * @returns Normalized tasks from latest synthesis
476
+ */
477
+ function extractTasksFromSyntheses(syntheses: RoundSynthesis[]): NormalizedTask[] {
478
+ if (syntheses.length === 0) return [];
479
+
480
+ const latestSynthesis = syntheses[syntheses.length - 1];
481
+
482
+ // NEW schema: Extract tasks from solutions
483
+ if (latestSynthesis.solutions && Array.isArray(latestSynthesis.solutions)) {
484
+ const allTasks: NormalizedTask[] = [];
485
+
486
+ for (const solution of latestSynthesis.solutions) {
487
+ const implPlan = solution.implementation_plan;
488
+ if (!implPlan?.tasks || !Array.isArray(implPlan.tasks)) continue;
489
+
490
+ for (const task of implPlan.tasks) {
491
+ const normalizedTask = normalizeSolutionTask(task, solution);
492
+ if (normalizedTask) {
493
+ allTasks.push(normalizedTask);
494
+ }
495
+ }
496
+ }
497
+
498
+ // Sort by task ID
499
+ return allTasks.sort((a, b) => {
500
+ const aNum = parseInt(a.id?.replace(/\D/g, '') || '0');
501
+ const bNum = parseInt(b.id?.replace(/\D/g, '') || '0');
502
+ return aNum - bNum;
503
+ });
504
+ }
505
+
506
+ // OLD schema: Extract from tasks array directly
507
+ const tasks = latestSynthesis.tasks;
508
+ if (!Array.isArray(tasks)) return [];
509
+
510
+ return tasks
511
+ .map((task) => normalizeTask(task))
512
+ .filter((task): task is NormalizedTask => task !== null);
513
+ }
514
+
515
+ /**
516
+ * Normalize a solution task from NEW schema to NormalizedTask
517
+ * @param task - SolutionTask from new schema
518
+ * @param solution - Parent solution for context
519
+ * @returns Normalized task
520
+ */
521
+ function normalizeSolutionTask(task: SolutionTask, solution: Solution): NormalizedTask | null {
522
+ if (!task || !task.id) return null;
523
+
524
+ return {
525
+ id: task.id,
526
+ title: task.name || 'Untitled Task',
527
+ status: (task as unknown as { status?: string }).status || 'pending',
528
+ meta: {
529
+ type: 'implementation',
530
+ agent: null,
531
+ scope: solution.name || null,
532
+ module: null
533
+ },
534
+ context: {
535
+ requirements: task.key_point ? [task.key_point] : [],
536
+ focus_paths: task.files?.map(f => f.file) || [],
537
+ acceptance: [],
538
+ depends_on: task.depends_on || []
539
+ },
540
+ flow_control: {
541
+ implementation_approach: task.files?.map((f, i) => ({
542
+ step: `Step ${i + 1}`,
543
+ action: `${f.action} ${f.file}${f.line ? ` at line ${f.line}` : ''}`
544
+ })) || []
545
+ },
546
+ _raw: {
547
+ task,
548
+ solution: {
549
+ name: solution.name,
550
+ source_cli: solution.source_cli,
551
+ feasibility: solution.feasibility,
552
+ effort: solution.effort,
553
+ risk: solution.risk
554
+ }
555
+ }
556
+ };
557
+ }
558
+
559
+ /**
560
+ * Normalize tasks from plan.json format to NormalizedTask[]
561
+ * plan.json tasks have: id, name, description, depends_on, status, files, key_point, acceptance_criteria
562
+ * @param tasks - Tasks array from plan.json
563
+ * @returns Normalized tasks
564
+ */
565
+ function normalizePlanJsonTasks(tasks: unknown[]): NormalizedTask[] {
566
+ if (!Array.isArray(tasks)) return [];
567
+
568
+ return tasks.map((task: any): NormalizedTask | null => {
569
+ if (!task || !task.id) return null;
570
+
571
+ return {
572
+ id: task.id,
573
+ title: task.name || task.title || 'Untitled Task',
574
+ status: task.status || 'pending',
575
+ meta: {
576
+ type: 'implementation',
577
+ agent: null,
578
+ scope: task.scope || null,
579
+ module: null
580
+ },
581
+ context: {
582
+ requirements: task.description ? [task.description] : (task.key_point ? [task.key_point] : []),
583
+ focus_paths: task.files?.map((f: any) => typeof f === 'string' ? f : f.file) || [],
584
+ acceptance: task.acceptance_criteria || [],
585
+ depends_on: task.depends_on || []
586
+ },
587
+ flow_control: {
588
+ implementation_approach: task.files?.map((f: any, i: number) => {
589
+ const filePath = typeof f === 'string' ? f : f.file;
590
+ const action = typeof f === 'string' ? 'modify' : f.action;
591
+ const line = typeof f === 'string' ? null : f.line;
592
+ return {
593
+ step: `Step ${i + 1}`,
594
+ action: `${action} ${filePath}${line ? ` at line ${line}` : ''}`
595
+ };
596
+ }) || []
597
+ },
598
+ _raw: {
599
+ task,
600
+ estimated_complexity: task.estimated_complexity
601
+ }
602
+ };
603
+ }).filter((task): task is NormalizedTask => task !== null);
604
+ }
605
+
145
606
  /**
146
607
  * Load plan.json or fix-plan.json from session directory
147
608
  * @param sessionPath - Session directory path
@@ -368,14 +829,19 @@ function calculateProgress(tasks: NormalizedTask[]): Progress {
368
829
  /**
369
830
  * Get detailed lite task info
370
831
  * @param workflowDir - Workflow directory
371
- * @param type - 'lite-plan' or 'lite-fix'
832
+ * @param type - 'lite-plan', 'lite-fix', or 'multi-cli-plan'
372
833
  * @param sessionId - Session ID
373
834
  * @returns Detailed task info
374
835
  */
375
836
  export async function getLiteTaskDetail(workflowDir: string, type: string, sessionId: string): Promise<LiteTaskDetail | null> {
376
- const dir = type === 'lite-plan'
377
- ? join(workflowDir, '.lite-plan', sessionId)
378
- : join(workflowDir, '.lite-fix', sessionId);
837
+ let dir: string;
838
+ if (type === 'lite-plan') {
839
+ dir = join(workflowDir, '.lite-plan', sessionId);
840
+ } else if (type === 'multi-cli-plan') {
841
+ dir = join(workflowDir, '.multi-cli-plan', sessionId);
842
+ } else {
843
+ dir = join(workflowDir, '.lite-fix', sessionId);
844
+ }
379
845
 
380
846
  try {
381
847
  const stats = await stat(dir);
@@ -384,6 +850,29 @@ export async function getLiteTaskDetail(workflowDir: string, type: string, sessi
384
850
  return null;
385
851
  }
386
852
 
853
+ // For multi-cli-plan, use synthesis-based loading
854
+ if (type === 'multi-cli-plan') {
855
+ const [syntheses, explorations, clarifications] = await Promise.all([
856
+ loadRoundSyntheses(dir),
857
+ loadExplorations(dir),
858
+ loadClarifications(dir),
859
+ ]);
860
+
861
+ const latestSynthesis = syntheses.length > 0 ? syntheses[syntheses.length - 1] : null;
862
+
863
+ const detail: LiteTaskDetail = {
864
+ id: sessionId,
865
+ type,
866
+ path: dir,
867
+ plan: latestSynthesis,
868
+ tasks: extractTasksFromSyntheses(syntheses),
869
+ explorations,
870
+ clarifications,
871
+ };
872
+
873
+ return detail;
874
+ }
875
+
387
876
  const [plan, tasks, explorations, clarifications, diagnoses] = await Promise.all([
388
877
  loadPlanJson(dir),
389
878
  loadTaskJsons(dir),