claude-code-workflow 6.3.26 → 6.3.28

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 (129) 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 +141 -163
  21. package/.claude/commands/workflow/lite-lite-lite.md +798 -0
  22. package/.claude/commands/workflow/multi-cli-plan.md +510 -0
  23. package/.claude/skills/ccw/SKILL.md +262 -372
  24. package/.claude/skills/ccw/command.json +547 -0
  25. package/.claude/skills/ccw-help/SKILL.md +46 -107
  26. package/.claude/skills/ccw-help/command.json +511 -0
  27. package/.claude/skills/skill-tuning/SKILL.md +303 -0
  28. package/.claude/skills/skill-tuning/phases/actions/action-abort.md +164 -0
  29. package/.claude/skills/skill-tuning/phases/actions/action-analyze-requirements.md +406 -0
  30. package/.claude/skills/skill-tuning/phases/actions/action-apply-fix.md +206 -0
  31. package/.claude/skills/skill-tuning/phases/actions/action-complete.md +195 -0
  32. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-agent.md +317 -0
  33. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-context.md +243 -0
  34. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-dataflow.md +318 -0
  35. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-docs.md +299 -0
  36. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-memory.md +269 -0
  37. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-token-consumption.md +200 -0
  38. package/.claude/skills/skill-tuning/phases/actions/action-gemini-analysis.md +322 -0
  39. package/.claude/skills/skill-tuning/phases/actions/action-generate-report.md +228 -0
  40. package/.claude/skills/skill-tuning/phases/actions/action-init.md +149 -0
  41. package/.claude/skills/skill-tuning/phases/actions/action-propose-fixes.md +317 -0
  42. package/.claude/skills/skill-tuning/phases/actions/action-verify.md +222 -0
  43. package/.claude/skills/skill-tuning/phases/orchestrator.md +377 -0
  44. package/.claude/skills/skill-tuning/phases/state-schema.md +378 -0
  45. package/.claude/skills/skill-tuning/specs/category-mappings.json +284 -0
  46. package/.claude/skills/skill-tuning/specs/dimension-mapping.md +212 -0
  47. package/.claude/skills/skill-tuning/specs/problem-taxonomy.md +318 -0
  48. package/.claude/skills/skill-tuning/specs/quality-gates.md +263 -0
  49. package/.claude/skills/skill-tuning/specs/skill-authoring-principles.md +189 -0
  50. package/.claude/skills/skill-tuning/specs/tuning-strategies.md +1537 -0
  51. package/.claude/skills/skill-tuning/templates/diagnosis-report.md +153 -0
  52. package/.claude/skills/skill-tuning/templates/fix-proposal.md +204 -0
  53. package/.claude/workflows/cli-templates/schemas/multi-cli-discussion-schema.json +421 -0
  54. package/.claude/workflows/cli-tools-usage.md +0 -41
  55. package/ccw/dist/core/auth/csrf-middleware.d.ts.map +1 -1
  56. package/ccw/dist/core/auth/csrf-middleware.js +3 -1
  57. package/ccw/dist/core/auth/csrf-middleware.js.map +1 -1
  58. package/ccw/dist/core/data-aggregator.d.ts +2 -0
  59. package/ccw/dist/core/data-aggregator.d.ts.map +1 -1
  60. package/ccw/dist/core/data-aggregator.js +5 -2
  61. package/ccw/dist/core/data-aggregator.js.map +1 -1
  62. package/ccw/dist/core/lite-scanner.d.ts +2 -1
  63. package/ccw/dist/core/lite-scanner.d.ts.map +1 -1
  64. package/ccw/dist/core/lite-scanner.js +295 -6
  65. package/ccw/dist/core/lite-scanner.js.map +1 -1
  66. package/ccw/dist/core/routes/codexlens/config-handlers.d.ts.map +1 -1
  67. package/ccw/dist/core/routes/codexlens/config-handlers.js +5 -5
  68. package/ccw/dist/core/routes/codexlens/config-handlers.js.map +1 -1
  69. package/ccw/dist/core/routes/session-routes.d.ts.map +1 -1
  70. package/ccw/dist/core/routes/session-routes.js +166 -48
  71. package/ccw/dist/core/routes/session-routes.js.map +1 -1
  72. package/ccw/dist/core/routes/system-routes.d.ts.map +1 -1
  73. package/ccw/dist/core/routes/system-routes.js +87 -0
  74. package/ccw/dist/core/routes/system-routes.js.map +1 -1
  75. package/ccw/dist/core/server.js +2 -2
  76. package/ccw/dist/core/server.js.map +1 -1
  77. package/ccw/scripts/IMPLEMENTATION-SUMMARY.md +226 -0
  78. package/ccw/scripts/QUICK-REFERENCE.md +135 -0
  79. package/ccw/scripts/README-memory-embedder.md +157 -0
  80. package/ccw/scripts/__pycache__/memory_embedder.cpython-313.pyc +0 -0
  81. package/ccw/scripts/__pycache__/test_memory_embedder.cpython-313-pytest-8.4.2.pyc +0 -0
  82. package/ccw/scripts/memory-embedder-example.ts +184 -0
  83. package/ccw/scripts/memory_embedder.py +428 -0
  84. package/ccw/scripts/test_memory_embedder.py +245 -0
  85. package/ccw/src/core/auth/csrf-middleware.ts +3 -1
  86. package/ccw/src/core/data-aggregator.ts +7 -2
  87. package/ccw/src/core/lite-scanner.ts +440 -6
  88. package/ccw/src/core/routes/codexlens/config-handlers.ts +12 -9
  89. package/ccw/src/core/routes/session-routes.ts +201 -48
  90. package/ccw/src/core/routes/system-routes.ts +102 -0
  91. package/ccw/src/core/server.ts +2 -2
  92. package/ccw/src/templates/dashboard-css/01-base.css +8 -0
  93. package/ccw/src/templates/dashboard-css/02-session.css +81 -0
  94. package/ccw/src/templates/dashboard-css/04-lite-tasks.css +2442 -0
  95. package/ccw/src/templates/dashboard-css/21-cli-toolmgmt.css +157 -0
  96. package/ccw/src/templates/dashboard-css/32-issue-manager.css +23 -0
  97. package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +38 -4
  98. package/ccw/src/templates/dashboard-js/components/hook-manager.js +38 -13
  99. package/ccw/src/templates/dashboard-js/components/navigation.js +24 -4
  100. package/ccw/src/templates/dashboard-js/i18n.js +194 -6
  101. package/ccw/src/templates/dashboard-js/views/api-settings.js +32 -0
  102. package/ccw/src/templates/dashboard-js/views/claude-manager.js +44 -3
  103. package/ccw/src/templates/dashboard-js/views/cli-manager.js +303 -31
  104. package/ccw/src/templates/dashboard-js/views/history.js +44 -6
  105. package/ccw/src/templates/dashboard-js/views/home.js +1 -0
  106. package/ccw/src/templates/dashboard-js/views/issue-manager.js +54 -7
  107. package/ccw/src/templates/dashboard-js/views/lite-tasks.js +1817 -4
  108. package/ccw/src/templates/dashboard.html +5 -0
  109. package/package.json +2 -1
  110. package/.claude/skills/ccw/index/command-capabilities.json +0 -127
  111. package/.claude/skills/ccw/index/intent-rules.json +0 -136
  112. package/.claude/skills/ccw/index/workflow-chains.json +0 -451
  113. package/.claude/skills/ccw/phases/actions/bugfix.md +0 -218
  114. package/.claude/skills/ccw/phases/actions/coupled.md +0 -194
  115. package/.claude/skills/ccw/phases/actions/docs.md +0 -93
  116. package/.claude/skills/ccw/phases/actions/full.md +0 -154
  117. package/.claude/skills/ccw/phases/actions/issue.md +0 -201
  118. package/.claude/skills/ccw/phases/actions/rapid.md +0 -104
  119. package/.claude/skills/ccw/phases/actions/review-fix.md +0 -84
  120. package/.claude/skills/ccw/phases/actions/tdd.md +0 -66
  121. package/.claude/skills/ccw/phases/actions/ui.md +0 -79
  122. package/.claude/skills/ccw/phases/orchestrator.md +0 -435
  123. package/.claude/skills/ccw/specs/intent-classification.md +0 -336
  124. package/.claude/skills/ccw-help/index/all-agents.json +0 -82
  125. package/.claude/skills/ccw-help/index/all-commands.json +0 -882
  126. package/.claude/skills/ccw-help/index/by-category.json +0 -914
  127. package/.claude/skills/ccw-help/index/by-use-case.json +0 -896
  128. package/.claude/skills/ccw-help/index/command-relationships.json +0 -160
  129. package/.claude/skills/ccw-help/index/essential-commands.json +0 -112
@@ -2,14 +2,29 @@
2
2
  * Session Routes Module
3
3
  * Handles all Session/Task-related API endpoints
4
4
  */
5
- import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
5
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
6
+ import { readFile, readdir, access } from 'fs/promises';
6
7
  import { join } from 'path';
7
8
  import type { RouteContext } from './types.js';
8
9
 
9
10
  /**
10
- * Get session detail data (context, summaries, impl-plan, review)
11
+ * Check if a file or directory exists (async version)
12
+ * @param filePath - Path to check
13
+ * @returns Promise<boolean>
14
+ */
15
+ async function fileExists(filePath: string): Promise<boolean> {
16
+ try {
17
+ await access(filePath);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Get session detail data (context, summaries, impl-plan, review, multi-cli)
11
26
  * @param {string} sessionPath - Path to session directory
12
- * @param {string} dataType - Type of data to load ('all', 'context', 'tasks', 'summary', 'plan', 'explorations', 'conflict', 'impl-plan', 'review')
27
+ * @param {string} dataType - Type of data to load ('all', 'context', 'tasks', 'summary', 'plan', 'explorations', 'conflict', 'impl-plan', 'review', 'multi-cli', 'discussions')
13
28
  * @returns {Promise<Object>}
14
29
  */
15
30
  async function getSessionDetailData(sessionPath: string, dataType: string): Promise<Record<string, unknown>> {
@@ -23,14 +38,15 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
23
38
  if (dataType === 'context' || dataType === 'all') {
24
39
  // Try .process/context-package.json first (common location)
25
40
  let contextFile = join(normalizedPath, '.process', 'context-package.json');
26
- if (!existsSync(contextFile)) {
41
+ if (!(await fileExists(contextFile))) {
27
42
  // Fallback to session root
28
43
  contextFile = join(normalizedPath, 'context-package.json');
29
44
  }
30
- if (existsSync(contextFile)) {
45
+ if (await fileExists(contextFile)) {
31
46
  try {
32
- result.context = JSON.parse(readFileSync(contextFile, 'utf8'));
47
+ result.context = JSON.parse(await readFile(contextFile, 'utf8'));
33
48
  } catch (e) {
49
+ console.warn('Failed to parse context file:', contextFile, (e as Error).message);
34
50
  result.context = null;
35
51
  }
36
52
  }
@@ -40,18 +56,18 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
40
56
  if (dataType === 'tasks' || dataType === 'all') {
41
57
  const taskDir = join(normalizedPath, '.task');
42
58
  result.tasks = [];
43
- if (existsSync(taskDir)) {
44
- const files = readdirSync(taskDir).filter(f => f.endsWith('.json') && f.startsWith('IMPL-'));
59
+ if (await fileExists(taskDir)) {
60
+ const files = (await readdir(taskDir)).filter(f => f.endsWith('.json') && f.startsWith('IMPL-'));
45
61
  for (const file of files) {
46
62
  try {
47
- const content = JSON.parse(readFileSync(join(taskDir, file), 'utf8'));
63
+ const content = JSON.parse(await readFile(join(taskDir, file), 'utf8'));
48
64
  result.tasks.push({
49
65
  filename: file,
50
66
  task_id: file.replace('.json', ''),
51
67
  ...content
52
68
  });
53
69
  } catch (e) {
54
- // Skip unreadable files
70
+ console.warn('Failed to parse task file:', join(taskDir, file), (e as Error).message);
55
71
  }
56
72
  }
57
73
  // Sort by task ID
@@ -63,14 +79,14 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
63
79
  if (dataType === 'summary' || dataType === 'all') {
64
80
  const summariesDir = join(normalizedPath, '.summaries');
65
81
  result.summaries = [];
66
- if (existsSync(summariesDir)) {
67
- const files = readdirSync(summariesDir).filter(f => f.endsWith('.md'));
82
+ if (await fileExists(summariesDir)) {
83
+ const files = (await readdir(summariesDir)).filter(f => f.endsWith('.md'));
68
84
  for (const file of files) {
69
85
  try {
70
- const content = readFileSync(join(summariesDir, file), 'utf8');
86
+ const content = await readFile(join(summariesDir, file), 'utf8');
71
87
  result.summaries.push({ name: file.replace('.md', ''), content });
72
88
  } catch (e) {
73
- // Skip unreadable files
89
+ console.warn('Failed to read summary file:', join(summariesDir, file), (e as Error).message);
74
90
  }
75
91
  }
76
92
  }
@@ -79,10 +95,11 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
79
95
  // Load plan.json (for lite tasks)
80
96
  if (dataType === 'plan' || dataType === 'all') {
81
97
  const planFile = join(normalizedPath, 'plan.json');
82
- if (existsSync(planFile)) {
98
+ if (await fileExists(planFile)) {
83
99
  try {
84
- result.plan = JSON.parse(readFileSync(planFile, 'utf8'));
100
+ result.plan = JSON.parse(await readFile(planFile, 'utf8'));
85
101
  } catch (e) {
102
+ console.warn('Failed to parse plan file:', planFile, (e as Error).message);
86
103
  result.plan = null;
87
104
  }
88
105
  }
@@ -100,52 +117,54 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
100
117
  ];
101
118
 
102
119
  for (const searchDir of searchDirs) {
103
- if (!existsSync(searchDir)) continue;
120
+ if (!(await fileExists(searchDir))) continue;
104
121
 
105
122
  // Look for explorations-manifest.json
106
123
  const manifestFile = join(searchDir, 'explorations-manifest.json');
107
- if (existsSync(manifestFile)) {
124
+ if (await fileExists(manifestFile)) {
108
125
  try {
109
- result.explorations.manifest = JSON.parse(readFileSync(manifestFile, 'utf8'));
126
+ result.explorations.manifest = JSON.parse(await readFile(manifestFile, 'utf8'));
110
127
 
111
128
  // Load each exploration file based on manifest
112
129
  const explorations = result.explorations.manifest.explorations || [];
113
130
  for (const exp of explorations) {
114
131
  const expFile = join(searchDir, exp.file);
115
- if (existsSync(expFile)) {
132
+ if (await fileExists(expFile)) {
116
133
  try {
117
- result.explorations.data[exp.angle] = JSON.parse(readFileSync(expFile, 'utf8'));
134
+ result.explorations.data[exp.angle] = JSON.parse(await readFile(expFile, 'utf8'));
118
135
  } catch (e) {
119
- // Skip unreadable exploration files
136
+ console.warn('Failed to parse exploration file:', expFile, (e as Error).message);
120
137
  }
121
138
  }
122
139
  }
123
140
  break; // Found manifest, stop searching
124
141
  } catch (e) {
142
+ console.warn('Failed to parse explorations manifest:', manifestFile, (e as Error).message);
125
143
  result.explorations.manifest = null;
126
144
  }
127
145
  }
128
146
 
129
147
  // Look for diagnoses-manifest.json
130
148
  const diagManifestFile = join(searchDir, 'diagnoses-manifest.json');
131
- if (existsSync(diagManifestFile)) {
149
+ if (await fileExists(diagManifestFile)) {
132
150
  try {
133
- result.diagnoses.manifest = JSON.parse(readFileSync(diagManifestFile, 'utf8'));
151
+ result.diagnoses.manifest = JSON.parse(await readFile(diagManifestFile, 'utf8'));
134
152
 
135
153
  // Load each diagnosis file based on manifest
136
154
  const diagnoses = result.diagnoses.manifest.diagnoses || [];
137
155
  for (const diag of diagnoses) {
138
156
  const diagFile = join(searchDir, diag.file);
139
- if (existsSync(diagFile)) {
157
+ if (await fileExists(diagFile)) {
140
158
  try {
141
- result.diagnoses.data[diag.angle] = JSON.parse(readFileSync(diagFile, 'utf8'));
159
+ result.diagnoses.data[diag.angle] = JSON.parse(await readFile(diagFile, 'utf8'));
142
160
  } catch (e) {
143
- // Skip unreadable diagnosis files
161
+ console.warn('Failed to parse diagnosis file:', diagFile, (e as Error).message);
144
162
  }
145
163
  }
146
164
  }
147
165
  break; // Found manifest, stop searching
148
166
  } catch (e) {
167
+ console.warn('Failed to parse diagnoses manifest:', diagManifestFile, (e as Error).message);
149
168
  result.diagnoses.manifest = null;
150
169
  }
151
170
  }
@@ -153,7 +172,7 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
153
172
  // Fallback: scan for exploration-*.json and diagnosis-*.json files directly
154
173
  if (!result.explorations.manifest) {
155
174
  try {
156
- const expFiles = readdirSync(searchDir).filter(f => f.startsWith('exploration-') && f.endsWith('.json') && f !== 'explorations-manifest.json');
175
+ const expFiles = (await readdir(searchDir)).filter(f => f.startsWith('exploration-') && f.endsWith('.json') && f !== 'explorations-manifest.json');
157
176
  if (expFiles.length > 0) {
158
177
  // Create synthetic manifest
159
178
  result.explorations.manifest = {
@@ -169,21 +188,21 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
169
188
  for (const file of expFiles) {
170
189
  const angle = file.replace('exploration-', '').replace('.json', '');
171
190
  try {
172
- result.explorations.data[angle] = JSON.parse(readFileSync(join(searchDir, file), 'utf8'));
191
+ result.explorations.data[angle] = JSON.parse(await readFile(join(searchDir, file), 'utf8'));
173
192
  } catch (e) {
174
- // Skip unreadable files
193
+ console.warn('Failed to parse exploration file:', join(searchDir, file), (e as Error).message);
175
194
  }
176
195
  }
177
196
  }
178
197
  } catch (e) {
179
- // Directory read failed
198
+ console.warn('Failed to read explorations directory:', searchDir, (e as Error).message);
180
199
  }
181
200
  }
182
201
 
183
202
  // Fallback: scan for diagnosis-*.json files directly
184
203
  if (!result.diagnoses.manifest) {
185
204
  try {
186
- const diagFiles = readdirSync(searchDir).filter(f => f.startsWith('diagnosis-') && f.endsWith('.json') && f !== 'diagnoses-manifest.json');
205
+ const diagFiles = (await readdir(searchDir)).filter(f => f.startsWith('diagnosis-') && f.endsWith('.json') && f !== 'diagnoses-manifest.json');
187
206
  if (diagFiles.length > 0) {
188
207
  // Create synthetic manifest
189
208
  result.diagnoses.manifest = {
@@ -199,14 +218,14 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
199
218
  for (const file of diagFiles) {
200
219
  const angle = file.replace('diagnosis-', '').replace('.json', '');
201
220
  try {
202
- result.diagnoses.data[angle] = JSON.parse(readFileSync(join(searchDir, file), 'utf8'));
221
+ result.diagnoses.data[angle] = JSON.parse(await readFile(join(searchDir, file), 'utf8'));
203
222
  } catch (e) {
204
- // Skip unreadable files
223
+ console.warn('Failed to parse diagnosis file:', join(searchDir, file), (e as Error).message);
205
224
  }
206
225
  }
207
226
  }
208
227
  } catch (e) {
209
- // Directory read failed
228
+ console.warn('Failed to read diagnoses directory:', searchDir, (e as Error).message);
210
229
  }
211
230
  }
212
231
 
@@ -228,12 +247,12 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
228
247
  ];
229
248
 
230
249
  for (const conflictFile of conflictFiles) {
231
- if (existsSync(conflictFile)) {
250
+ if (await fileExists(conflictFile)) {
232
251
  try {
233
- result.conflictResolution = JSON.parse(readFileSync(conflictFile, 'utf8'));
252
+ result.conflictResolution = JSON.parse(await readFile(conflictFile, 'utf8'));
234
253
  break; // Found file, stop searching
235
254
  } catch (e) {
236
- // Skip unreadable file
255
+ console.warn('Failed to parse conflict resolution file:', conflictFile, (e as Error).message);
237
256
  }
238
257
  }
239
258
  }
@@ -242,15 +261,149 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
242
261
  // Load IMPL_PLAN.md
243
262
  if (dataType === 'impl-plan' || dataType === 'all') {
244
263
  const implPlanFile = join(normalizedPath, 'IMPL_PLAN.md');
245
- if (existsSync(implPlanFile)) {
264
+ if (await fileExists(implPlanFile)) {
246
265
  try {
247
- result.implPlan = readFileSync(implPlanFile, 'utf8');
266
+ result.implPlan = await readFile(implPlanFile, 'utf8');
248
267
  } catch (e) {
268
+ console.warn('Failed to read IMPL_PLAN.md:', implPlanFile, (e as Error).message);
249
269
  result.implPlan = null;
250
270
  }
251
271
  }
252
272
  }
253
273
 
274
+ // Load multi-cli discussion rounds (rounds/*/synthesis.json)
275
+ // Supports both NEW and OLD schema formats
276
+ if (dataType === 'multi-cli' || dataType === 'discussions' || dataType === 'all') {
277
+ result.multiCli = {
278
+ sessionId: normalizedPath.split('/').pop() || '',
279
+ type: 'multi-cli-plan',
280
+ rounds: [] as Array<{
281
+ roundNumber: number;
282
+ synthesis: Record<string, unknown> | null;
283
+ // NEW schema extracted fields
284
+ solutions?: Array<{
285
+ name: string;
286
+ source_cli: string[];
287
+ feasibility: number;
288
+ effort: string;
289
+ risk: string;
290
+ summary: string;
291
+ tasksCount: number;
292
+ dependencies: { internal: string[]; external: string[] };
293
+ technical_concerns: string[];
294
+ }>;
295
+ convergence?: {
296
+ score: number;
297
+ new_insights: boolean;
298
+ recommendation: string;
299
+ };
300
+ cross_verification?: {
301
+ agreements: string[];
302
+ disagreements: string[];
303
+ resolution: string;
304
+ };
305
+ clarification_questions?: string[];
306
+ }>,
307
+ // Aggregated data from latest synthesis
308
+ latestSolutions: [] as Array<Record<string, unknown>>,
309
+ latestConvergence: null as Record<string, unknown> | null,
310
+ latestCrossVerification: null as Record<string, unknown> | null,
311
+ clarificationQuestions: [] as string[]
312
+ };
313
+
314
+ const roundsDir = join(normalizedPath, 'rounds');
315
+ if (await fileExists(roundsDir)) {
316
+ try {
317
+ const roundDirs = (await readdir(roundsDir))
318
+ .filter(d => /^\d+$/.test(d)) // Only numeric directories
319
+ .sort((a, b) => parseInt(a) - parseInt(b));
320
+
321
+ for (const roundDir of roundDirs) {
322
+ const synthesisFile = join(roundsDir, roundDir, 'synthesis.json');
323
+ let synthesis: Record<string, unknown> | null = null;
324
+
325
+ if (await fileExists(synthesisFile)) {
326
+ try {
327
+ synthesis = JSON.parse(await readFile(synthesisFile, 'utf8'));
328
+ } catch (e) {
329
+ console.warn('Failed to parse synthesis file:', synthesisFile, (e as Error).message);
330
+ }
331
+ }
332
+
333
+ // Build round data with NEW schema fields extracted
334
+ const roundData: any = {
335
+ roundNumber: parseInt(roundDir),
336
+ synthesis
337
+ };
338
+
339
+ // Extract NEW schema fields if present
340
+ if (synthesis) {
341
+ // Extract solutions with summary info
342
+ if (Array.isArray(synthesis.solutions)) {
343
+ roundData.solutions = (synthesis.solutions as Array<Record<string, any>>).map(s => ({
344
+ name: s.name || '',
345
+ source_cli: s.source_cli || [],
346
+ feasibility: s.feasibility ?? 0,
347
+ effort: s.effort || 'unknown',
348
+ risk: s.risk || 'unknown',
349
+ summary: s.summary || '',
350
+ tasksCount: s.implementation_plan?.tasks?.length || 0,
351
+ dependencies: s.dependencies || { internal: [], external: [] },
352
+ technical_concerns: s.technical_concerns || []
353
+ }));
354
+ }
355
+
356
+ // Extract convergence
357
+ if (synthesis.convergence && typeof synthesis.convergence === 'object') {
358
+ const conv = synthesis.convergence as Record<string, unknown>;
359
+ roundData.convergence = {
360
+ score: conv.score ?? 0,
361
+ new_insights: conv.new_insights ?? false,
362
+ recommendation: conv.recommendation || 'unknown'
363
+ };
364
+ }
365
+
366
+ // Extract cross_verification
367
+ if (synthesis.cross_verification && typeof synthesis.cross_verification === 'object') {
368
+ const cv = synthesis.cross_verification as Record<string, unknown>;
369
+ roundData.cross_verification = {
370
+ agreements: Array.isArray(cv.agreements) ? cv.agreements : [],
371
+ disagreements: Array.isArray(cv.disagreements) ? cv.disagreements : [],
372
+ resolution: (cv.resolution as string) || ''
373
+ };
374
+ }
375
+
376
+ // Extract clarification_questions
377
+ if (Array.isArray(synthesis.clarification_questions)) {
378
+ roundData.clarification_questions = synthesis.clarification_questions;
379
+ }
380
+ }
381
+
382
+ result.multiCli.rounds.push(roundData);
383
+ }
384
+
385
+ // Populate aggregated data from latest round
386
+ if (result.multiCli.rounds.length > 0) {
387
+ const latestRound = result.multiCli.rounds[result.multiCli.rounds.length - 1];
388
+ if (latestRound.solutions) {
389
+ result.multiCli.latestSolutions = latestRound.solutions;
390
+ }
391
+ if (latestRound.convergence) {
392
+ result.multiCli.latestConvergence = latestRound.convergence;
393
+ }
394
+ if (latestRound.cross_verification) {
395
+ result.multiCli.latestCrossVerification = latestRound.cross_verification;
396
+ }
397
+ if (latestRound.clarification_questions) {
398
+ result.multiCli.clarificationQuestions = latestRound.clarification_questions;
399
+ }
400
+ }
401
+ } catch (e) {
402
+ console.warn('Failed to read rounds directory:', roundsDir, (e as Error).message);
403
+ }
404
+ }
405
+ }
406
+
254
407
  // Load review data from .review/
255
408
  if (dataType === 'review' || dataType === 'all') {
256
409
  const reviewDir = join(normalizedPath, '.review');
@@ -261,12 +414,12 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
261
414
  totalFindings: 0
262
415
  };
263
416
 
264
- if (existsSync(reviewDir)) {
417
+ if (await fileExists(reviewDir)) {
265
418
  // Load review-state.json
266
419
  const stateFile = join(reviewDir, 'review-state.json');
267
- if (existsSync(stateFile)) {
420
+ if (await fileExists(stateFile)) {
268
421
  try {
269
- const state = JSON.parse(readFileSync(stateFile, 'utf8'));
422
+ const state = JSON.parse(await readFile(stateFile, 'utf8'));
270
423
  result.review.state = state;
271
424
  result.review.severityDistribution = state.severity_distribution || {};
272
425
  result.review.totalFindings = state.total_findings || 0;
@@ -275,18 +428,18 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
275
428
  result.review.crossCuttingConcerns = state.cross_cutting_concerns || [];
276
429
  result.review.criticalFiles = state.critical_files || [];
277
430
  } catch (e) {
278
- // Skip unreadable state
431
+ console.warn('Failed to parse review state file:', stateFile, (e as Error).message);
279
432
  }
280
433
  }
281
434
 
282
435
  // Load dimension findings
283
436
  const dimensionsDir = join(reviewDir, 'dimensions');
284
- if (existsSync(dimensionsDir)) {
285
- const files = readdirSync(dimensionsDir).filter(f => f.endsWith('.json'));
437
+ if (await fileExists(dimensionsDir)) {
438
+ const files = (await readdir(dimensionsDir)).filter(f => f.endsWith('.json'));
286
439
  for (const file of files) {
287
440
  try {
288
441
  const dimName = file.replace('.json', '');
289
- const data = JSON.parse(readFileSync(join(dimensionsDir, file), 'utf8'));
442
+ const data = JSON.parse(await readFile(join(dimensionsDir, file), 'utf8'));
290
443
 
291
444
  // Handle array structure: [ { findings: [...] } ]
292
445
  let findings = [];
@@ -308,7 +461,7 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
308
461
  count: findings.length
309
462
  });
310
463
  } catch (e) {
311
- // Skip unreadable files
464
+ console.warn('Failed to parse review dimension file:', join(dimensionsDir, file), (e as Error).message);
312
465
  }
313
466
  }
314
467
  }
@@ -416,5 +416,107 @@ export async function handleSystemRoutes(ctx: SystemRouteContext): Promise<boole
416
416
  return true;
417
417
  }
418
418
 
419
+ // API: File dialog - list directory contents for file browser
420
+ if (pathname === '/api/dialog/browse' && req.method === 'POST') {
421
+ handlePostRequest(req, res, async (body) => {
422
+ const { path: browsePath, showHidden } = body as {
423
+ path?: string;
424
+ showHidden?: boolean;
425
+ };
426
+
427
+ const os = await import('os');
428
+ const path = await import('path');
429
+ const fs = await import('fs');
430
+
431
+ // Default to home directory
432
+ let targetPath = browsePath || os.homedir();
433
+
434
+ // Expand ~ to home directory
435
+ if (targetPath.startsWith('~')) {
436
+ targetPath = path.join(os.homedir(), targetPath.slice(1));
437
+ }
438
+
439
+ // Resolve to absolute path
440
+ if (!path.isAbsolute(targetPath)) {
441
+ targetPath = path.resolve(targetPath);
442
+ }
443
+
444
+ try {
445
+ const stat = await fs.promises.stat(targetPath);
446
+ if (!stat.isDirectory()) {
447
+ return { error: 'Path is not a directory', status: 400 };
448
+ }
449
+
450
+ const entries = await fs.promises.readdir(targetPath, { withFileTypes: true });
451
+ const items = entries
452
+ .filter(entry => showHidden || !entry.name.startsWith('.'))
453
+ .map(entry => ({
454
+ name: entry.name,
455
+ path: path.join(targetPath, entry.name),
456
+ isDirectory: entry.isDirectory(),
457
+ isFile: entry.isFile()
458
+ }))
459
+ .sort((a, b) => {
460
+ // Directories first, then files
461
+ if (a.isDirectory && !b.isDirectory) return -1;
462
+ if (!a.isDirectory && b.isDirectory) return 1;
463
+ return a.name.localeCompare(b.name);
464
+ });
465
+
466
+ return {
467
+ currentPath: targetPath,
468
+ parentPath: path.dirname(targetPath),
469
+ items,
470
+ homePath: os.homedir()
471
+ };
472
+ } catch (err) {
473
+ return { error: 'Cannot access directory: ' + (err as Error).message, status: 400 };
474
+ }
475
+ });
476
+ return true;
477
+ }
478
+
479
+ // API: File dialog - select file (validate path exists)
480
+ if (pathname === '/api/dialog/open-file' && req.method === 'POST') {
481
+ handlePostRequest(req, res, async (body) => {
482
+ const { path: filePath } = body as { path?: string };
483
+
484
+ if (!filePath) {
485
+ return { error: 'Path is required', status: 400 };
486
+ }
487
+
488
+ const os = await import('os');
489
+ const path = await import('path');
490
+ const fs = await import('fs');
491
+
492
+ let targetPath = filePath;
493
+
494
+ // Expand ~ to home directory
495
+ if (targetPath.startsWith('~')) {
496
+ targetPath = path.join(os.homedir(), targetPath.slice(1));
497
+ }
498
+
499
+ // Resolve to absolute path
500
+ if (!path.isAbsolute(targetPath)) {
501
+ targetPath = path.resolve(targetPath);
502
+ }
503
+
504
+ try {
505
+ await fs.promises.access(targetPath, fs.constants.R_OK);
506
+ const stat = await fs.promises.stat(targetPath);
507
+
508
+ return {
509
+ success: true,
510
+ path: targetPath,
511
+ isFile: stat.isFile(),
512
+ isDirectory: stat.isDirectory()
513
+ };
514
+ } catch {
515
+ return { error: 'File not accessible', status: 404 };
516
+ }
517
+ });
518
+ return true;
519
+ }
520
+
419
521
  return false;
420
522
  }
@@ -597,12 +597,12 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
597
597
  if (await handleFilesRoutes(routeContext)) return;
598
598
  }
599
599
 
600
- // System routes (data, health, version, paths, shutdown, notify, storage)
600
+ // System routes (data, health, version, paths, shutdown, notify, storage, dialog)
601
601
  if (pathname === '/api/data' || pathname === '/api/health' ||
602
602
  pathname === '/api/version-check' || pathname === '/api/shutdown' ||
603
603
  pathname === '/api/recent-paths' || pathname === '/api/switch-path' ||
604
604
  pathname === '/api/remove-recent-path' || pathname === '/api/system/notify' ||
605
- pathname.startsWith('/api/storage/')) {
605
+ pathname.startsWith('/api/storage/') || pathname.startsWith('/api/dialog/')) {
606
606
  if (await handleSystemRoutes(routeContext)) return;
607
607
  }
608
608
 
@@ -119,6 +119,14 @@ body {
119
119
  color: hsl(var(--orange));
120
120
  }
121
121
 
122
+ .nav-item[data-lite="multi-cli-plan"].active {
123
+ background-color: hsl(var(--purple-light, 280 60% 95%));
124
+ }
125
+
126
+ .nav-item[data-lite="multi-cli-plan"].active .nav-icon {
127
+ color: hsl(var(--purple, 280 60% 50%));
128
+ }
129
+
122
130
  .sidebar.collapsed .toggle-icon {
123
131
  transform: rotate(180deg);
124
132
  }
@@ -102,6 +102,87 @@
102
102
  color: hsl(220 80% 40%);
103
103
  }
104
104
 
105
+ /* Session Status Badge (used in detail page header) */
106
+ .session-status-badge {
107
+ font-size: 0.7rem;
108
+ font-weight: 500;
109
+ padding: 0.25rem 0.625rem;
110
+ border-radius: 0.25rem;
111
+ text-transform: lowercase;
112
+ }
113
+
114
+ .session-status-badge.plan_generated,
115
+ .session-status-badge.converged,
116
+ .session-status-badge.completed,
117
+ .session-status-badge.decided {
118
+ background: hsl(var(--success-light, 142 70% 95%));
119
+ color: hsl(var(--success, 142 70% 45%));
120
+ }
121
+
122
+ .session-status-badge.analyzing,
123
+ .session-status-badge.debating {
124
+ background: hsl(var(--warning-light, 45 90% 95%));
125
+ color: hsl(var(--warning, 45 90% 40%));
126
+ }
127
+
128
+ .session-status-badge.initialized,
129
+ .session-status-badge.exploring {
130
+ background: hsl(var(--info-light, 220 80% 95%));
131
+ color: hsl(var(--info, 220 80% 55%));
132
+ }
133
+
134
+ .session-status-badge.blocked,
135
+ .session-status-badge.conflict {
136
+ background: hsl(var(--destructive) / 0.1);
137
+ color: hsl(var(--destructive));
138
+ }
139
+
140
+ .session-status-badge.pending {
141
+ background: hsl(var(--muted));
142
+ color: hsl(var(--muted-foreground));
143
+ }
144
+
145
+ /* Status Badge Colors (used in card list meta) */
146
+ .session-meta-item.status-badge.success {
147
+ background: hsl(var(--success-light, 142 70% 95%));
148
+ color: hsl(var(--success, 142 70% 45%));
149
+ padding: 0.25rem 0.5rem;
150
+ border-radius: 0.25rem;
151
+ font-weight: 500;
152
+ }
153
+
154
+ .session-meta-item.status-badge.warning {
155
+ background: hsl(var(--warning-light, 45 90% 95%));
156
+ color: hsl(var(--warning, 45 90% 40%));
157
+ padding: 0.25rem 0.5rem;
158
+ border-radius: 0.25rem;
159
+ font-weight: 500;
160
+ }
161
+
162
+ .session-meta-item.status-badge.info {
163
+ background: hsl(var(--info-light, 220 80% 95%));
164
+ color: hsl(var(--info, 220 80% 55%));
165
+ padding: 0.25rem 0.5rem;
166
+ border-radius: 0.25rem;
167
+ font-weight: 500;
168
+ }
169
+
170
+ .session-meta-item.status-badge.error {
171
+ background: hsl(var(--destructive) / 0.1);
172
+ color: hsl(var(--destructive));
173
+ padding: 0.25rem 0.5rem;
174
+ border-radius: 0.25rem;
175
+ font-weight: 500;
176
+ }
177
+
178
+ .session-meta-item.status-badge.default {
179
+ background: hsl(var(--muted));
180
+ color: hsl(var(--muted-foreground));
181
+ padding: 0.25rem 0.5rem;
182
+ border-radius: 0.25rem;
183
+ font-weight: 500;
184
+ }
185
+
105
186
  .session-body {
106
187
  display: flex;
107
188
  flex-direction: column;