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.
- package/.claude/CLAUDE.md +7 -1
- package/.claude/agents/action-planning-agent.md +1 -0
- package/.claude/agents/cli-discuss-agent.md +391 -0
- package/.claude/agents/cli-execution-agent.md +2 -0
- package/.claude/agents/cli-explore-agent.md +2 -1
- package/.claude/agents/cli-lite-planning-agent.md +1 -0
- package/.claude/agents/cli-planning-agent.md +1 -0
- package/.claude/agents/code-developer.md +1 -0
- package/.claude/agents/conceptual-planning-agent.md +2 -0
- package/.claude/agents/context-search-agent.md +1 -0
- package/.claude/agents/debug-explore-agent.md +2 -0
- package/.claude/agents/doc-generator.md +1 -0
- package/.claude/agents/issue-plan-agent.md +2 -1
- package/.claude/agents/issue-queue-agent.md +2 -1
- package/.claude/agents/memory-bridge.md +2 -0
- package/.claude/agents/test-context-search-agent.md +2 -0
- package/.claude/agents/test-fix-agent.md +1 -0
- package/.claude/agents/ui-design-agent.md +2 -0
- package/.claude/agents/universal-executor.md +1 -0
- package/.claude/commands/issue/execute.md +141 -163
- package/.claude/commands/workflow/lite-lite-lite.md +798 -0
- package/.claude/commands/workflow/multi-cli-plan.md +510 -0
- package/.claude/skills/ccw/SKILL.md +262 -372
- package/.claude/skills/ccw/command.json +547 -0
- package/.claude/skills/ccw-help/SKILL.md +46 -107
- package/.claude/skills/ccw-help/command.json +511 -0
- package/.claude/skills/skill-tuning/SKILL.md +303 -0
- package/.claude/skills/skill-tuning/phases/actions/action-abort.md +164 -0
- package/.claude/skills/skill-tuning/phases/actions/action-analyze-requirements.md +406 -0
- package/.claude/skills/skill-tuning/phases/actions/action-apply-fix.md +206 -0
- package/.claude/skills/skill-tuning/phases/actions/action-complete.md +195 -0
- package/.claude/skills/skill-tuning/phases/actions/action-diagnose-agent.md +317 -0
- package/.claude/skills/skill-tuning/phases/actions/action-diagnose-context.md +243 -0
- package/.claude/skills/skill-tuning/phases/actions/action-diagnose-dataflow.md +318 -0
- package/.claude/skills/skill-tuning/phases/actions/action-diagnose-docs.md +299 -0
- package/.claude/skills/skill-tuning/phases/actions/action-diagnose-memory.md +269 -0
- package/.claude/skills/skill-tuning/phases/actions/action-diagnose-token-consumption.md +200 -0
- package/.claude/skills/skill-tuning/phases/actions/action-gemini-analysis.md +322 -0
- package/.claude/skills/skill-tuning/phases/actions/action-generate-report.md +228 -0
- package/.claude/skills/skill-tuning/phases/actions/action-init.md +149 -0
- package/.claude/skills/skill-tuning/phases/actions/action-propose-fixes.md +317 -0
- package/.claude/skills/skill-tuning/phases/actions/action-verify.md +222 -0
- package/.claude/skills/skill-tuning/phases/orchestrator.md +377 -0
- package/.claude/skills/skill-tuning/phases/state-schema.md +378 -0
- package/.claude/skills/skill-tuning/specs/category-mappings.json +284 -0
- package/.claude/skills/skill-tuning/specs/dimension-mapping.md +212 -0
- package/.claude/skills/skill-tuning/specs/problem-taxonomy.md +318 -0
- package/.claude/skills/skill-tuning/specs/quality-gates.md +263 -0
- package/.claude/skills/skill-tuning/specs/skill-authoring-principles.md +189 -0
- package/.claude/skills/skill-tuning/specs/tuning-strategies.md +1537 -0
- package/.claude/skills/skill-tuning/templates/diagnosis-report.md +153 -0
- package/.claude/skills/skill-tuning/templates/fix-proposal.md +204 -0
- package/.claude/workflows/cli-templates/schemas/multi-cli-discussion-schema.json +421 -0
- package/.claude/workflows/cli-tools-usage.md +0 -41
- package/ccw/dist/core/auth/csrf-middleware.d.ts.map +1 -1
- package/ccw/dist/core/auth/csrf-middleware.js +3 -1
- package/ccw/dist/core/auth/csrf-middleware.js.map +1 -1
- package/ccw/dist/core/data-aggregator.d.ts +2 -0
- package/ccw/dist/core/data-aggregator.d.ts.map +1 -1
- package/ccw/dist/core/data-aggregator.js +5 -2
- package/ccw/dist/core/data-aggregator.js.map +1 -1
- package/ccw/dist/core/lite-scanner.d.ts +2 -1
- package/ccw/dist/core/lite-scanner.d.ts.map +1 -1
- package/ccw/dist/core/lite-scanner.js +295 -6
- package/ccw/dist/core/lite-scanner.js.map +1 -1
- package/ccw/dist/core/routes/codexlens/config-handlers.d.ts.map +1 -1
- package/ccw/dist/core/routes/codexlens/config-handlers.js +5 -5
- package/ccw/dist/core/routes/codexlens/config-handlers.js.map +1 -1
- package/ccw/dist/core/routes/session-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/session-routes.js +166 -48
- package/ccw/dist/core/routes/session-routes.js.map +1 -1
- package/ccw/dist/core/routes/system-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/system-routes.js +87 -0
- package/ccw/dist/core/routes/system-routes.js.map +1 -1
- package/ccw/dist/core/server.js +2 -2
- package/ccw/dist/core/server.js.map +1 -1
- package/ccw/scripts/IMPLEMENTATION-SUMMARY.md +226 -0
- package/ccw/scripts/QUICK-REFERENCE.md +135 -0
- package/ccw/scripts/README-memory-embedder.md +157 -0
- package/ccw/scripts/__pycache__/memory_embedder.cpython-313.pyc +0 -0
- package/ccw/scripts/__pycache__/test_memory_embedder.cpython-313-pytest-8.4.2.pyc +0 -0
- package/ccw/scripts/memory-embedder-example.ts +184 -0
- package/ccw/scripts/memory_embedder.py +428 -0
- package/ccw/scripts/test_memory_embedder.py +245 -0
- package/ccw/src/core/auth/csrf-middleware.ts +3 -1
- package/ccw/src/core/data-aggregator.ts +7 -2
- package/ccw/src/core/lite-scanner.ts +440 -6
- package/ccw/src/core/routes/codexlens/config-handlers.ts +12 -9
- package/ccw/src/core/routes/session-routes.ts +201 -48
- package/ccw/src/core/routes/system-routes.ts +102 -0
- package/ccw/src/core/server.ts +2 -2
- package/ccw/src/templates/dashboard-css/01-base.css +8 -0
- package/ccw/src/templates/dashboard-css/02-session.css +81 -0
- package/ccw/src/templates/dashboard-css/04-lite-tasks.css +2442 -0
- package/ccw/src/templates/dashboard-css/21-cli-toolmgmt.css +157 -0
- package/ccw/src/templates/dashboard-css/32-issue-manager.css +23 -0
- package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +38 -4
- package/ccw/src/templates/dashboard-js/components/hook-manager.js +38 -13
- package/ccw/src/templates/dashboard-js/components/navigation.js +24 -4
- package/ccw/src/templates/dashboard-js/i18n.js +194 -6
- package/ccw/src/templates/dashboard-js/views/api-settings.js +32 -0
- package/ccw/src/templates/dashboard-js/views/claude-manager.js +44 -3
- package/ccw/src/templates/dashboard-js/views/cli-manager.js +303 -31
- package/ccw/src/templates/dashboard-js/views/history.js +44 -6
- package/ccw/src/templates/dashboard-js/views/home.js +1 -0
- package/ccw/src/templates/dashboard-js/views/issue-manager.js +54 -7
- package/ccw/src/templates/dashboard-js/views/lite-tasks.js +1817 -4
- package/ccw/src/templates/dashboard.html +5 -0
- package/package.json +2 -1
- package/.claude/skills/ccw/index/command-capabilities.json +0 -127
- package/.claude/skills/ccw/index/intent-rules.json +0 -136
- package/.claude/skills/ccw/index/workflow-chains.json +0 -451
- package/.claude/skills/ccw/phases/actions/bugfix.md +0 -218
- package/.claude/skills/ccw/phases/actions/coupled.md +0 -194
- package/.claude/skills/ccw/phases/actions/docs.md +0 -93
- package/.claude/skills/ccw/phases/actions/full.md +0 -154
- package/.claude/skills/ccw/phases/actions/issue.md +0 -201
- package/.claude/skills/ccw/phases/actions/rapid.md +0 -104
- package/.claude/skills/ccw/phases/actions/review-fix.md +0 -84
- package/.claude/skills/ccw/phases/actions/tdd.md +0 -66
- package/.claude/skills/ccw/phases/actions/ui.md +0 -79
- package/.claude/skills/ccw/phases/orchestrator.md +0 -435
- package/.claude/skills/ccw/specs/intent-classification.md +0 -336
- package/.claude/skills/ccw-help/index/all-agents.json +0 -82
- package/.claude/skills/ccw-help/index/all-commands.json +0 -882
- package/.claude/skills/ccw-help/index/by-category.json +0 -914
- package/.claude/skills/ccw-help/index/by-use-case.json +0 -896
- package/.claude/skills/ccw-help/index/command-relationships.json +0 -160
- 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
|
|
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
|
-
*
|
|
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 (!
|
|
41
|
+
if (!(await fileExists(contextFile))) {
|
|
27
42
|
// Fallback to session root
|
|
28
43
|
contextFile = join(normalizedPath, 'context-package.json');
|
|
29
44
|
}
|
|
30
|
-
if (
|
|
45
|
+
if (await fileExists(contextFile)) {
|
|
31
46
|
try {
|
|
32
|
-
result.context = JSON.parse(
|
|
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 (
|
|
44
|
-
const files =
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
67
|
-
const files =
|
|
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 =
|
|
86
|
+
const content = await readFile(join(summariesDir, file), 'utf8');
|
|
71
87
|
result.summaries.push({ name: file.replace('.md', ''), content });
|
|
72
88
|
} catch (e) {
|
|
73
|
-
|
|
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 (
|
|
98
|
+
if (await fileExists(planFile)) {
|
|
83
99
|
try {
|
|
84
|
-
result.plan = JSON.parse(
|
|
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 (!
|
|
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 (
|
|
124
|
+
if (await fileExists(manifestFile)) {
|
|
108
125
|
try {
|
|
109
|
-
result.explorations.manifest = JSON.parse(
|
|
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 (
|
|
132
|
+
if (await fileExists(expFile)) {
|
|
116
133
|
try {
|
|
117
|
-
result.explorations.data[exp.angle] = JSON.parse(
|
|
134
|
+
result.explorations.data[exp.angle] = JSON.parse(await readFile(expFile, 'utf8'));
|
|
118
135
|
} catch (e) {
|
|
119
|
-
|
|
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 (
|
|
149
|
+
if (await fileExists(diagManifestFile)) {
|
|
132
150
|
try {
|
|
133
|
-
result.diagnoses.manifest = JSON.parse(
|
|
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 (
|
|
157
|
+
if (await fileExists(diagFile)) {
|
|
140
158
|
try {
|
|
141
|
-
result.diagnoses.data[diag.angle] = JSON.parse(
|
|
159
|
+
result.diagnoses.data[diag.angle] = JSON.parse(await readFile(diagFile, 'utf8'));
|
|
142
160
|
} catch (e) {
|
|
143
|
-
|
|
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 =
|
|
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(
|
|
191
|
+
result.explorations.data[angle] = JSON.parse(await readFile(join(searchDir, file), 'utf8'));
|
|
173
192
|
} catch (e) {
|
|
174
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
221
|
+
result.diagnoses.data[angle] = JSON.parse(await readFile(join(searchDir, file), 'utf8'));
|
|
203
222
|
} catch (e) {
|
|
204
|
-
|
|
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
|
-
|
|
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 (
|
|
250
|
+
if (await fileExists(conflictFile)) {
|
|
232
251
|
try {
|
|
233
|
-
result.conflictResolution = JSON.parse(
|
|
252
|
+
result.conflictResolution = JSON.parse(await readFile(conflictFile, 'utf8'));
|
|
234
253
|
break; // Found file, stop searching
|
|
235
254
|
} catch (e) {
|
|
236
|
-
|
|
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 (
|
|
264
|
+
if (await fileExists(implPlanFile)) {
|
|
246
265
|
try {
|
|
247
|
-
result.implPlan =
|
|
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 (
|
|
417
|
+
if (await fileExists(reviewDir)) {
|
|
265
418
|
// Load review-state.json
|
|
266
419
|
const stateFile = join(reviewDir, 'review-state.json');
|
|
267
|
-
if (
|
|
420
|
+
if (await fileExists(stateFile)) {
|
|
268
421
|
try {
|
|
269
|
-
const state = JSON.parse(
|
|
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
|
-
|
|
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 (
|
|
285
|
-
const files =
|
|
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(
|
|
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
|
-
|
|
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
|
}
|
package/ccw/src/core/server.ts
CHANGED
|
@@ -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;
|