Skip to content

Commit 81702c9

Browse files
author
catlog22
committed
feat: Enhance installation and upgrade processes with ecosystem selection and global file management
- Added interactive target ecosystem selection during installation (`claude`, `codex`, or `all`). - Updated installation command to always install `claude.ccw.md` globally. - Modified upgrade command to prompt for migration of old `CLAUDE.md` to new format. - Improved handling of global files and user settings during installation and uninstallation. - Updated documentation to reflect new installation and upgrade features, including project-specific instructions referencing global CCW instructions.
1 parent bbfc294 commit 81702c9

25 files changed

Lines changed: 912 additions & 190 deletions

.claude/commands/workflow-tune.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,29 @@ if (stepsNeedTask.length > 0) {
192192
// test → 测试任务: "为{场景}的{某模块}编写测试,覆盖{具体场景}"
193193
// fix/debug → 修复任务: "先在沙箱创建含已知 bug 的代码,然后诊断修复"
194194
// refactor → 重构任务: "先在沙箱创建可工作但需重构的代码,然后重构"
195-
196-
step.test_task = /* 按上述模板生成,必须包含:项目、任务、功能点、技术约束、验收标准 */;
197-
step.acceptance_criteria = /* 从 test_task 中提取 2-4 条可验证标准 */;
195+
//
196+
// ★ UPSTREAM-SCOPE RULE(通用原则):
197+
// 若本步命令的实际能力边界由**上游产物**决定(上一步生成了清单/列表/计划/任务集,
198+
// 本步要"跑完"这个清单),那么 test_task 的范围必须**严格对齐上游产物的全量**,
199+
// 不得凭空虚构"只做其中一部分"的子集。
200+
//
201+
// 判断信号:上一步 step 的命令名或描述中含 plan/list/catalog/queue/todo/spec/manifest 等
202+
// 产物类词汇,且本步命令描述中出现 execute/run/process/consume/iterate/dispatch 等消费动词。
203+
//
204+
// 为什么:收窄范围会让被测命令的"调度器/依赖解析/并行分发/run-to-completion/批次推进"
205+
// 等关键行为完全不被触发 — 测试只能证明"它能做一件事",无法证明"它能跑完一批"。
206+
//
207+
// 正确做法:功能点写"按上游清单顺序/依赖执行全部条目",验收标准包含"产物覆盖率 ≥ N%";
208+
// 错误做法:从上游清单里点名 1-2 条作为功能点 — 会让命令退化到单点模式。
209+
const hasUpstreamScope = stepIdx > 0
210+
&& /plan|list|catalog|queue|todo|spec|manifest|清单|计划|任务/i.test(
211+
(steps[stepIdx - 1]?.command || '') + ' ' + (steps[stepIdx - 1]?.test_task || ''))
212+
&& /execute|run|process|consume|iterate|dispatch|assemble|build|执行|运行|组装/i.test(cmdDesc);
213+
214+
step.test_task = /* 按上述模板生成,必须包含:项目、任务、功能点、技术约束、验收标准。
215+
若 hasUpstreamScope,功能点必须描述"全量消费上游产物"而非挑选子集 */;
216+
step.acceptance_criteria = /* 从 test_task 中提取 2-4 条可验证标准。
217+
若 hasUpstreamScope,至少 1 条必须是"产物覆盖率 / 全量完成度" */;
198218
step.complexity_level = /plan|design|architect/i.test(cmdDesc) ? 'high'
199219
: /test|lint|format/i.test(cmdDesc) ? 'low' : 'medium';
200220
}
@@ -552,6 +572,9 @@ const prompt = assembleStepPrompt(step, stepIdx, state);
552572
553573
// ★ All steps execute via ccw cli --tool claude --mode write
554574
// ★ --cd 指向沙箱目录(独立项目),不影响真实工作空间
575+
// ★★★ ONE STEP = ONE CLI CALL — 绝对禁止将多个 step 合并到一次 ccw cli 调用中
576+
// ★★★ 即使步骤紧密关联(如 plan+execute、execute+quality+review),也必须逐个独立调用
577+
// ★★★ 每次 Bash(ccw cli ...) 调用只处理 steps[stepIdx] 这一个步骤,不可批量
555578
Bash({
556579
command: `ccw cli -p ${escapeForShell(prompt)} --tool claude --mode write --rule universal-rigorous-style --cd "${state.sandbox_dir}"`,
557580
run_in_background: true, timeout: 600000
@@ -809,3 +832,4 @@ Report → local generation (no CLI call)
809832
8. **Artifact Collection**: Scan sandbox filesystem (not git diff), compare pre/post snapshots
810833
9. **Prompt Assembly**: Every step goes through `assembleStepPrompt()` — resolves command file, reads YAML metadata, injects test_task, builds rich context
811834
10. **Auto-Confirm**: All prompts auto-confirmed, no blocking interactions during execution
835+
11. **ONE STEP = ONE CLI CALL (no multi-step batching)**: Each step MUST get exactly one independent `ccw cli --tool claude --mode write` invocation. Combining multiple steps (e.g. plan+execute, execute+quality+review) into a single CLI call is strictly prohibited. Violations cause: (1) later steps get skipped or truncated, (2) per-step quality analysis impossible, (3) timeout risk. This is a P0 rule — never batch steps.

_b64.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
eyJwZXJzcGVjdGl2ZSI6IkFyY2hpdGVjdHVyYWwgLSBTeXN0ZW0gZGVzaWduLCBkYXRhIGZsb3csIHNjYWxhYmlsaXR5LCBjb21wb25lbnQgaW50ZXJhY3Rpb25zLCByZXNpbGllbmNlIHBhdHRlcm5zIiwicmVsZXZhbnRfZmlsZXMiOlt7InBhdGgiOiJjY3cvc3JjL2NvcmUvcm91dGVzL2FuYWx5c2lzLXJvdXRlcy50cyIsInJlbGV2YW5jZSI6MC45NSwicmF0aW9uYWxlIjoiQU5MLSogc2Nhbm5pbmcgQVBJIHdpdGggc2luZ2xlLWZpbGUgc3RhdHVzIGRlcGVuZGVuY3kgYW5kIG5vIGNvbnRlbnQgcmVjb3ZlcnkiLCJyb2xlIjoibW9kaWZ5X3RhcmdldCIsImRpc2NvdmVyeV9zb3VyY2UiOiJiYXNoLXNjYW4iLCJ0b3BpY19yZWxhdGlvbiI6IkFOTC0qIHNlc3Npb25zIGhhdmUgd2Vha2VzdCByZXNpbGllbmNlOiBzdGF0dXMgZnJvbSBjb25jbHVzaW9ucy5qc29uIGFsb25lIiwia2V5X2NvZGUiOlt7InN5bWJvbCI6ImdldFNlc3Npb25TdW1tYXJ5IiwibG9jYXRpb24iOiJMODYtTDExMyIsImRlc2NyaXB0aW9uIjoiU3RhdHVzIGZyb20gY29uY2x1c2lvbnMuanNvbi4gQ29ycnVwdGVkPWluX3Byb2dyZXNzIHdpdGggc2x1ZyBuYW1lIn0seyJzeW1ib2wiOiJnZXRTZXNzaW9uRGV0YWlsIiwibG9jYXRpb24iOiJMMTE4LUwxNDgiLCJkZXNjcmlwdGlvbiI6IjQgZmlsZXMgcGFyYWxsZWwsIG51bGwgb24gZXJyb3IsIG5vIHBhcnRpYWwgaW5kaWNhdG9ycyJ9LHsic3ltYm9sIjoicmVhZEpzb25GaWxlL3JlYWRUZXh0RmlsZSIsImxvY2F0aW9uIjoiTDYxLUw4MSIsImRlc2NyaXB0aW9uIjoiTnVsbCBvbiBhbnkgZXJyb3IsIHNpbGVudGx5IHN3YWxsb3dlZCJ9XX1dfQ==

_writer.cjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const fs = require("fs");
2+
const p = "D:/Claude_dms3/.workflow/.analysis/ANL-frontend-dashboard-artifact-robustness-2026-04-11/explorations/Architectural.json";
3+
writeFile(p, "test", "utf8");
4+
console.log("ok");

_writer.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const fs = require("fs");
2+
const p = "D:/Claude_dms3/.workflow/.analysis/ANL-frontend-dashboard-artifact-robustness-2026-04-11/explorations/Architectural.json";
3+
writeFile(p, "test", "utf8");
4+
console.log("ok");

ccw/frontend/src/components/shared/SessionCard.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
Clock,
2727
CheckCircle2,
2828
AlertCircle,
29+
AlertTriangle,
2930
RefreshCw,
3031
FileText,
3132
Search,
@@ -229,6 +230,11 @@ export function SessionCard({
229230
const isPlanning = session.status === 'planning';
230231
const isArchived = session.status === 'archived' || session.location === 'archived';
231232

233+
// Data tier awareness
234+
const dataTier = session.dataTier ?? 0;
235+
const isTier3Compact = dataTier >= 3; // Minimal info: name + date only
236+
const isTier2Degraded = dataTier === 2; // Limited Data indicator
237+
232238
const handleCardClick = (e: React.MouseEvent) => {
233239
// Don't trigger if clicking on dropdown
234240
if ((e.target as HTMLElement).closest('[data-radix-popper-content-wrapper]')) {
@@ -255,6 +261,36 @@ export function SessionCard({
255261
}
256262
};
257263

264+
// Tier 3: Compact card showing only session name and created date
265+
if (isTier3Compact) {
266+
return (
267+
<Card
268+
className={cn(
269+
'group cursor-pointer transition-all duration-200 hover:shadow-md hover:border-primary/30 opacity-70',
270+
className
271+
)}
272+
onClick={handleCardClick}
273+
>
274+
<CardContent className="p-3">
275+
<div className="flex items-center justify-between gap-2">
276+
<h3 className="font-bold text-card-foreground text-xs tracking-wide uppercase truncate">
277+
{session.session_id}
278+
</h3>
279+
<Badge variant="secondary" className="gap-1 flex-shrink-0 text-[10px]">
280+
<AlertTriangle className="h-3 w-3" />
281+
Minimal Info
282+
</Badge>
283+
</div>
284+
{session.created_at && (
285+
<p className="text-xs text-muted-foreground mt-1">
286+
{formatDate(session.created_at)}
287+
</p>
288+
)}
289+
</CardContent>
290+
</Card>
291+
);
292+
}
293+
258294
return (
259295
<Card
260296
className={cn(
@@ -265,6 +301,14 @@ export function SessionCard({
265301
onClick={handleCardClick}
266302
>
267303
<CardContent className="p-4">
304+
{/* Data degradation indicator for Tier 2 */}
305+
{isTier2Degraded && (
306+
<div className="flex items-center gap-1 mb-2 text-xs text-muted-foreground" data-degraded="true">
307+
<AlertTriangle className="h-3 w-3" />
308+
<span>Limited Data</span>
309+
</div>
310+
)}
311+
268312
{/* Header - Type badge + Session ID as title */}
269313
<div className="flex items-start justify-between gap-2 mb-2">
270314
<div className="flex-1 min-w-0">
@@ -283,6 +327,11 @@ export function SessionCard({
283327
</div>
284328
<div className="flex items-center gap-2 flex-shrink-0">
285329
<Badge variant={statusVariant}>{statusLabel}</Badge>
330+
{isTier2Degraded && (
331+
<Badge variant="secondary" className="gap-1 text-[10px]">
332+
Limited Data
333+
</Badge>
334+
)}
286335
{showActions && (
287336
<DropdownMenu>
288337
<DropdownMenuTrigger asChild>

ccw/frontend/src/components/team/TeamArtifacts.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,12 @@ export function TeamArtifacts({ teamName }: TeamArtifactsProps) {
213213
setContent(result.content);
214214
} catch (err) {
215215
console.error('Failed to load file content:', err);
216-
setContent(formatMessage({ id: 'team.artifacts.contentError', defaultMessage: 'Failed to load file content' }));
216+
const apiErr = err as { status?: number; path?: string; reason?: string };
217+
if (apiErr.status === 422 && apiErr.path && apiErr.reason) {
218+
setContent(`Error reading ${apiErr.path}: ${apiErr.reason}`);
219+
} else {
220+
setContent(formatMessage({ id: 'team.artifacts.contentError', defaultMessage: 'Failed to load file content' }));
221+
}
217222
} finally {
218223
setLoading(false);
219224
}

ccw/frontend/src/lib/api.ts

Lines changed: 78 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ interface BackendSessionData {
3535
type?: string;
3636
created_at: string;
3737
updated_at?: string;
38+
/** Data completeness tier (0=full, 1=partial, 2=stat-only, 3=name-only) */
39+
dataTier?: number;
3840
[key: string]: unknown;
3941
}
4042

@@ -99,6 +101,8 @@ export interface ApiError {
99101
message: string;
100102
status: number;
101103
code?: string;
104+
path?: string;
105+
reason?: string;
102106
}
103107

104108
// ========== CSRF Token Handling ==========
@@ -310,6 +314,8 @@ export async function fetchApi<T>(
310314
if (body.message) error.message = body.message;
311315
else if (body.error) error.message = body.error;
312316
if (body.code) error.code = body.code;
317+
if (body.path) error.path = body.path;
318+
if (body.reason) error.reason = body.reason;
313319
} catch (parseError) {
314320
// Silently ignore JSON parse errors for non-JSON responses
315321
}
@@ -471,6 +477,10 @@ function transformBackendSession(
471477
summaries: (backendSession as unknown as { summaries?: SessionMetadata['summaries'] }).summaries,
472478
tasks: ((backendSession as unknown as { tasks?: TaskData[] }).tasks || [])
473479
.map(t => normalizeTask(t as unknown as Record<string, unknown>)),
480+
// Pass through data tier from backend (clamped to valid range)
481+
dataTier: backendSession.dataTier != null
482+
? (Math.min(Math.max(backendSession.dataTier, 0), 3) as 0 | 1 | 2 | 3)
483+
: undefined,
474484
};
475485
}
476486

@@ -2175,12 +2185,45 @@ export interface SessionDetailResponse {
21752185

21762186
/**
21772187
* Fetch session detail for a specific workspace
2178-
* First fetches session list to get the session path, then fetches detail data
2188+
* Uses progressive path resolution: cache > by-id > fetchSessions fallback
21792189
* @param sessionId - Session ID to fetch details for
21802190
* @param projectPath - Optional project path to filter data by workspace
21812191
*/
21822192
export async function fetchSessionDetail(sessionId: string, projectPath?: string): Promise<SessionDetailResponse> {
2183-
// Step 1: Fetch all sessions to get the session path
2193+
// Phase 1: Try TanStack Query cache for session path (zero API calls)
2194+
let sessionPath: string | undefined;
2195+
let cachedSession: SessionMetadata | undefined;
2196+
2197+
if (projectPath) {
2198+
try {
2199+
// Dynamic import to avoid circular dependency - api.ts is imported by hooks
2200+
const { default: queryClient } = await import('./query-client');
2201+
const cacheKey = ['workspace', projectPath, 'sessions', 'list'] as const;
2202+
const cached = queryClient.getQueryData<{ activeSessions: SessionMetadata[]; archivedSessions: SessionMetadata[] }>(cacheKey);
2203+
if (cached) {
2204+
const allSessions = [...cached.activeSessions, ...cached.archivedSessions];
2205+
cachedSession = allSessions.find(s => s.session_id === sessionId);
2206+
if (cachedSession?.path) {
2207+
sessionPath = cachedSession.path;
2208+
}
2209+
}
2210+
} catch {
2211+
// queryClient not available (SSR, test env, etc.) - fall through to other strategies
2212+
}
2213+
}
2214+
2215+
// Phase 3: Try /api/session-detail?id= endpoint (1 API call)
2216+
if (!sessionPath) {
2217+
try {
2218+
const idParam = `/api/session-detail?id=${encodeURIComponent(sessionId)}&type=all${projectPath ? `&projectPath=${encodeURIComponent(projectPath)}` : ''}`;
2219+
const detailData = await fetchApi<Record<string, unknown>>(idParam);
2220+
return transformDetailResponse(detailData, cachedSession, sessionId);
2221+
} catch {
2222+
// by-id endpoint failed (session not found, network error) - fall through to fetchSessions
2223+
}
2224+
}
2225+
2226+
// Fallback: fetchSessions + session-detail (2 API calls, backward compatible)
21842227
const sessionsData = await fetchSessions(projectPath);
21852228
const allSessions = [...sessionsData.activeSessions, ...sessionsData.archivedSessions];
21862229
const session = allSessions.find(s => s.session_id === sessionId);
@@ -2189,40 +2232,54 @@ export async function fetchSessionDetail(sessionId: string, projectPath?: string
21892232
throw new Error(`Session not found: ${sessionId}`);
21902233
}
21912234

2192-
// Step 2: Use the session path to fetch detail data from the correct endpoint
2193-
// Backend expects the actual session directory path, not the project path
2194-
const sessionPath = (session as any).path || session.session_id;
2195-
const detailData = await fetchApi<any>(`/api/session-detail?path=${encodeURIComponent(sessionPath)}&type=all`);
2235+
// Use session path from fresh fetch
2236+
sessionPath = (session as any).path || session.session_id;
2237+
const detailData = await fetchApi<Record<string, unknown>>(`/api/session-detail?path=${encodeURIComponent(sessionPath!)}&type=all`);
2238+
return transformDetailResponse(detailData, session, sessionId);
2239+
}
21962240

2197-
// Step 3: Transform the response to match SessionDetailResponse interface
2198-
// Also check for summaries array and extract first one if summary is empty
2199-
let finalSummary = detailData.summary;
2200-
if (!finalSummary && detailData.summaries && detailData.summaries.length > 0) {
2201-
finalSummary = detailData.summaries[0].content || detailData.summaries[0].name || '';
2241+
/**
2242+
* Transform raw detail API response into SessionDetailResponse
2243+
*/
2244+
function transformDetailResponse(
2245+
detailData: Record<string, unknown>,
2246+
sessionMeta: SessionMetadata | undefined,
2247+
sessionId: string
2248+
): SessionDetailResponse {
2249+
// Extract summary: prefer direct summary, fallback to first summaries entry
2250+
let finalSummary = detailData.summary as string | undefined;
2251+
if (!finalSummary && Array.isArray(detailData.summaries) && detailData.summaries.length > 0) {
2252+
const first = detailData.summaries[0] as { content?: string; name?: string };
2253+
finalSummary = first.content || first.name || '';
22022254
}
22032255

2204-
// Step 4: Transform context to match SessionDetailContext interface
2205-
// Backend returns raw context-package.json content, frontend expects it nested under 'context' field
2256+
// Backend returns raw context-package.json content, frontend expects it nested under 'context'
22062257
const transformedContext = detailData.context ? { context: detailData.context } : undefined;
22072258

2208-
// Step 5: Merge tasks from detailData into session object
2209-
// Backend returns tasks at root level, frontend expects them on session object
2210-
const sessionWithTasks = {
2211-
...session,
2212-
tasks: detailData.tasks || session.tasks || [],
2213-
};
2259+
// Build session object: prefer metadata from cache/list, merge tasks from detail
2260+
const sessionWithTasks = sessionMeta
2261+
? { ...sessionMeta, tasks: (Array.isArray(detailData.tasks) ? detailData.tasks : sessionMeta.tasks || []) }
2262+
: {
2263+
session_id: sessionId,
2264+
title: sessionId,
2265+
status: 'in_progress' as const,
2266+
created_at: '',
2267+
location: 'active' as const,
2268+
tasks: Array.isArray(detailData.tasks) ? detailData.tasks : [],
2269+
};
22142270

22152271
return {
22162272
session: sessionWithTasks,
22172273
context: transformedContext,
22182274
summary: finalSummary,
2219-
summaries: detailData.summaries,
2275+
summaries: detailData.summaries as SessionDetailResponse['summaries'],
22202276
implPlan: detailData.implPlan,
2221-
conflicts: detailData.conflictResolution, // Backend returns 'conflictResolution', not 'conflicts'
2277+
conflicts: Array.isArray(detailData.conflictResolution) ? detailData.conflictResolution : undefined,
22222278
review: detailData.review,
22232279
};
22242280
}
22252281

2282+
22262283
// ========== History / CLI Execution API ==========
22272284

22282285
export interface CliExecution {

0 commit comments

Comments
 (0)