diff --git a/.codex/skills/analyze-with-file/EXECUTE.md b/.codex/skills/analyze-with-file/EXECUTE.md index c3f51f78..aaf25793 100644 --- a/.codex/skills/analyze-with-file/EXECUTE.md +++ b/.codex/skills/analyze-with-file/EXECUTE.md @@ -1,26 +1,25 @@ -# Analyze Task Generation Spec +# Analyze Task Generation & Execution Spec -> **Purpose**: Quality standards for converting `conclusions.json` recommendations into `.task/*.json` files. +> **Purpose**: Quality standards for task generation + execution specification for Phase 5 of `analyze-with-file`. > **Consumer**: Phase 5 of `analyze-with-file` workflow. -> **Execution**: Handled by `unified-execute-with-file` — this document covers generation only. +> **Scope**: Task generation quality + direct inline execution. --- ## Task Generation Flow +> **Entry point**: Routed here from SKILL.md Phase 5 when complexity is `complex` (≥3 recommendations or high-priority with dependencies). + ``` -conclusions.json ──┐ - ├── resolveTargetFiles() ──┐ -codebaseContext ───┘ ├── .task/TASK-001.json - ├── .task/TASK-002.json -explorations.json ── enrichContext() ─────────┘ +Step 1: Load context → Step 2: Generate .task/*.json → Step 3: Pre-execution analysis + → Step 4: User confirmation → Step 5: Serial execution → Step 6: Finalize ``` **Input artifacts** (all from session folder): | Artifact | Required | Provides | |----------|----------|----------| -| `conclusions.json` | Yes | `recommendations[]` with action, rationale, priority, target_files, changes, implementation_hints, evidence_refs | +| `conclusions.json` | Yes | `recommendations[]` with action, rationale, priority, evidence_refs | | `exploration-codebase.json` | No | `relevant_files[]`, `patterns[]`, `constraints[]`, `integration_points[]` — primary source for file resolution | | `explorations.json` | No | `sources[]`, `key_findings[]` — fallback for file resolution | | `perspectives.json` | No | Multi-perspective findings — alternative to explorations.json | @@ -29,18 +28,25 @@ explorations.json ── enrichContext() ─────────┘ ## File Resolution Algorithm -Target files are resolved with a 3-priority fallback chain: +Target files are resolved with a 3-priority fallback chain. Recommendations carry only `evidence_refs` — file resolution is EXECUTE.md's responsibility: ```javascript function resolveTargetFiles(rec, codebaseContext, explorations) { - // Priority 1: Explicit target_files from recommendations (Phase 4 enriched) - if (rec.target_files?.length) { - return rec.target_files.map(path => ({ - path, - action: 'modify', - target: null, - changes: rec.changes || [] - })) + // Priority 1: Extract file paths from evidence_refs (e.g., "src/auth/token.ts:89") + if (rec.evidence_refs?.length) { + const filePaths = [...new Set( + rec.evidence_refs + .filter(ref => ref.includes('/') || ref.includes('.')) + .map(ref => ref.split(':')[0]) + )] + if (filePaths.length) { + return filePaths.map(path => ({ + path, + action: 'modify', + target: null, + changes: [] + })) + } } // Priority 2: Match from exploration-codebase.json relevant_files @@ -133,8 +139,8 @@ function inferTaskType(rec) { | priority=low OR single file | `small` | ```javascript -function inferEffort(rec) { - const fileCount = rec.target_files?.length || 0 +function inferEffort(rec, targetFiles) { + const fileCount = targetFiles?.length || 0 if (rec.priority === 'high' && fileCount >= 3) return 'large' if (rec.priority === 'high' || fileCount >= 2) return 'medium' if (rec.priority === 'low' || fileCount <= 1) return 'small' @@ -252,7 +258,7 @@ function validateConvergenceQuality(tasks) { ## Required Task Fields (analyze-with-file producer) -Per `task-schema.json` `_field_usage_by_producer`, the `analyze-with-file` producer MUST populate: +SKILL.md produces minimal recommendations `{action, rationale, priority, evidence_refs}`. EXECUTE.md enriches these into full task JSON. The final `.task/*.json` MUST populate: | Block | Fields | Required | |-------|--------|----------| @@ -328,19 +334,383 @@ Per `task-schema.json` `_field_usage_by_producer`, the `analyze-with-file` produ --- -## Execution Delegation +## Step 1: Load All Context Sources -After `.task/*.json` generation, execution is handled by `unified-execute-with-file`: +Phase 2-4 already loaded and processed these artifacts. If data is still in conversation memory, skip disk reads. -```bash -/codex:unified-execute-with-file PLAN="${sessionFolder}/.task/" +```javascript +// Skip loading if already in memory from Phase 2-4 +// Only read from disk when entering EXECUTE.md from a fresh/resumed session + +if (!conclusions) { + conclusions = JSON.parse(Read(`${sessionFolder}/conclusions.json`)) +} + +if (!codebaseContext) { + codebaseContext = file_exists(`${sessionFolder}/exploration-codebase.json`) + ? JSON.parse(Read(`${sessionFolder}/exploration-codebase.json`)) + : null +} + +if (!explorations) { + explorations = file_exists(`${sessionFolder}/explorations.json`) + ? JSON.parse(Read(`${sessionFolder}/explorations.json`)) + : file_exists(`${sessionFolder}/perspectives.json`) + ? JSON.parse(Read(`${sessionFolder}/perspectives.json`)) + : null +} ``` -The execution engine provides: -- Pre-execution analysis (dependency validation, file conflicts, topological sort) -- Serial task execution with convergence verification -- Progress tracking via `execution.md` + `execution-events.md` -- Auto-commit per task (conventional commit format) -- Failure handling with retry/skip/abort options +## Step 2: Enrich Recommendations & Generate .task/*.json -**No inline execution logic in analyze-with-file** — single execution engine avoids duplication and ensures consistent behavior across all skill producers. +SKILL.md Phase 4 produces minimal recommendations: `{action, rationale, priority, evidence_refs}`. +This step enriches each recommendation with execution-specific details using codebase context, then generates individual task JSON files. + +**Enrichment pipeline**: `rec (minimal) + codebaseContext + explorations → task JSON (full)` + +```javascript +const tasks = conclusions.recommendations.map((rec, index) => { + const taskId = `TASK-${String(index + 1).padStart(3, '0')}` + + // 1. ENRICH: Resolve target files from codebase context (not from rec) + const targetFiles = resolveTargetFiles(rec, codebaseContext, explorations) + + // 2. ENRICH: Generate implementation steps from action + context + const implSteps = generateImplementationSteps(rec, targetFiles, codebaseContext) + + // 3. ENRICH: Derive change descriptions per file + const enrichedFiles = targetFiles.map(f => ({ + path: f.path, + action: f.action || 'modify', + target: f.target || null, + changes: deriveChanges(rec, f, codebaseContext) || [], + change: rec.action + })) + + return { + id: taskId, + title: rec.action, + description: rec.rationale, + type: inferTaskType(rec), + priority: rec.priority, + effort: inferEffort(rec, targetFiles), + + files: enrichedFiles, + depends_on: [], + + // CONVERGENCE (must pass quality validation) + convergence: { + criteria: generateCriteria(rec), + verification: generateVerification(rec), + definition_of_done: generateDoD(rec) + }, + + // IMPLEMENTATION steps (generated here, not from SKILL.md) + implementation: implSteps, + + // CONTEXT + evidence: rec.evidence_refs || [], + source: { + tool: 'analyze-with-file', + session_id: sessionId, + original_id: taskId + } + } +}) + +// Quality validation +validateConvergenceQuality(tasks) + +// Write each task as individual JSON file +Bash(`mkdir -p ${sessionFolder}/.task`) +tasks.forEach(task => { + Write(`${sessionFolder}/.task/${task.id}.json`, JSON.stringify(task, null, 2)) +}) +``` + +**Enrichment Functions**: + +```javascript +// Generate implementation steps from action + resolved files +function generateImplementationSteps(rec, targetFiles, codebaseContext) { + // 1. Parse rec.action into atomic steps + // 2. Map steps to target files + // 3. Add context from codebaseContext.patterns if applicable + // Return: [{step: '1', description: '...', actions: [...]}] + return [{ + step: '1', + description: rec.action, + actions: targetFiles.map(f => `Modify ${f.path}`) + }] +} + +// Derive specific change descriptions for a file +function deriveChanges(rec, file, codebaseContext) { + // 1. Match rec.action keywords to file content patterns + // 2. Use codebaseContext.patterns for context-aware change descriptions + // 3. Use rec.evidence_refs to locate specific modification points + // Return: ['specific change 1', 'specific change 2'] + return [rec.action] +} +``` + +## Step 3-6: Execution Steps + +After `.task/*.json` generation, validate and execute tasks directly inline. + +### Step 3: Pre-Execution Analysis + +```javascript +const taskFiles = Glob(`${sessionFolder}/.task/*.json`) +const tasks = taskFiles.map(f => JSON.parse(Read(f))) + +// 1. Dependency validation +const taskIds = new Set(tasks.map(t => t.id)) +const errors = [] +tasks.forEach(task => { + task.depends_on.forEach(dep => { + if (!taskIds.has(dep)) errors.push(`${task.id}: depends on unknown task ${dep}`) + }) +}) + +// 2. Circular dependency detection (DFS) +function detectCycles(tasks) { + const graph = new Map(tasks.map(t => [t.id, t.depends_on])) + const visited = new Set(), inStack = new Set(), cycles = [] + function dfs(node, path) { + if (inStack.has(node)) { cycles.push([...path, node].join(' → ')); return } + if (visited.has(node)) return + visited.add(node); inStack.add(node) + ;(graph.get(node) || []).forEach(dep => dfs(dep, [...path, node])) + inStack.delete(node) + } + tasks.forEach(t => { if (!visited.has(t.id)) dfs(t.id, []) }) + return cycles +} + +// 3. Topological sort for execution order +function topoSort(tasks) { + const inDegree = new Map(tasks.map(t => [t.id, 0])) + tasks.forEach(t => t.depends_on.forEach(dep => { + inDegree.set(t.id, inDegree.get(t.id) + 1) + })) + const queue = tasks.filter(t => inDegree.get(t.id) === 0).map(t => t.id) + const order = [] + while (queue.length) { + const id = queue.shift() + order.push(id) + tasks.forEach(t => { + if (t.depends_on.includes(id)) { + inDegree.set(t.id, inDegree.get(t.id) - 1) + if (inDegree.get(t.id) === 0) queue.push(t.id) + } + }) + } + return order +} + +// 4. File conflict detection +const fileTaskMap = new Map() +tasks.forEach(task => { + (task.files || []).forEach(f => { + if (!fileTaskMap.has(f.path)) fileTaskMap.set(f.path, []) + fileTaskMap.get(f.path).push(task.id) + }) +}) +const conflicts = [] +fileTaskMap.forEach((taskIds, file) => { + if (taskIds.length > 1) conflicts.push({ file, tasks: taskIds }) +}) +``` + +### Step 4: Initialize Execution Artifacts + +```javascript +// execution.md — overview with task table +const executionMd = `# Execution Overview + +## Session Info +- **Session ID**: ${sessionId} +- **Plan Source**: .task/*.json (from analysis conclusions) +- **Started**: ${getUtc8ISOString()} +- **Total Tasks**: ${tasks.length} + +## Task Overview + +| # | ID | Title | Type | Priority | Status | +|---|-----|-------|------|----------|--------| +${tasks.map((t, i) => `| ${i+1} | ${t.id} | ${t.title} | ${t.type} | ${t.priority} | pending |`).join('\n')} + +## Pre-Execution Analysis +${conflicts.length + ? `### File Conflicts\n${conflicts.map(c => `- **${c.file}**: ${c.tasks.join(', ')}`).join('\n')}` + : 'No file conflicts detected.'} + +## Execution Timeline +> Updated as tasks complete +` +Write(`${sessionFolder}/execution.md`, executionMd) + +// execution-events.md — chronological event log +Write(`${sessionFolder}/execution-events.md`, + `# Execution Events\n\n**Session**: ${sessionId}\n**Started**: ${getUtc8ISOString()}\n\n---\n\n`) +``` + +### Step 5: Task Execution Loop + +**User Confirmation** before execution: + +```javascript +if (!autoYes) { + const action = AskUserQuestion({ + questions: [{ + question: `Execute ${tasks.length} tasks?\n${tasks.map(t => ` ${t.id}: ${t.title} (${t.priority})`).join('\n')}`, + header: "Confirm", + multiSelect: false, + options: [ + { label: "Start", description: "Execute all tasks serially" }, + { label: "Adjust", description: "Modify .task/*.json before execution" }, + { label: "Skip", description: "Keep .task/*.json, skip execution" } + ] + }] + }) + // "Adjust": user edits task files, then resumes + // "Skip": end — user can execute later separately +} +``` + +Execute tasks serially using `task.implementation` steps and `task.files[].changes` as guidance. + +``` +For each taskId in executionOrder: + ├─ Load task from .task/{taskId}.json + ├─ Check dependencies satisfied + ├─ Record START event → execution-events.md + ├─ Execute using task.implementation + task.files[].changes: + │ ├─ Read target files listed in task.files[] + │ ├─ Apply modifications described in files[].changes / files[].change + │ ├─ Follow implementation[].actions sequence + │ └─ Use Edit (preferred), Write (new files), Bash (build/test) + ├─ Verify convergence: + │ ├─ Check each convergence.criteria[] item + │ ├─ Run convergence.verification (if executable command) + │ └─ Record verification results + ├─ Record COMPLETE/FAIL event → execution-events.md + ├─ Update execution.md task status + └─ Continue to next task +``` + +**Execution Guidance Priority** — what the AI follows when executing each task: + +| Priority | Source | Example | +|----------|--------|---------| +| 1 | `files[].changes` / `files[].change` | "Add await to refreshToken() call at line 89" | +| 2 | `implementation[].actions` | ["Read token.ts", "Add await keyword at line 89"] | +| 3 | `implementation[].description` | "Add await to refreshToken() call in token.ts" | +| 4 | `task.description` | "Token refresh fails silently..." | + +When `files[].changes` is populated, the AI has concrete instructions. When empty, it falls back to `implementation` steps, then to `description`. + +### Step 5.1: Failure Handling + +```javascript +// On task failure, ask user how to proceed +if (!autoYes) { + AskUserQuestion({ + questions: [{ + question: `Task ${task.id} failed: ${errorMessage}\nHow to proceed?`, + header: "Failure", + multiSelect: false, + options: [ + { label: "Skip & Continue", description: "Skip this task, continue with next" }, + { label: "Retry", description: "Retry this task" }, + { label: "Abort", description: "Stop execution, keep progress" } + ] + }] + }) +} +``` + +### Step 6: Finalize + +After all tasks complete: + +1. Append execution summary to `execution.md` (statistics, task results table) +2. Append session footer to `execution-events.md` +3. Write back `_execution` state to each `.task/*.json`: + +```javascript +tasks.forEach(task => { + const updated = { + ...task, + status: task._status, // "completed" | "failed" | "skipped" + executed_at: task._executed_at, + result: { + success: task._status === 'completed', + files_modified: task._result?.files_modified || [], + summary: task._result?.summary || '', + error: task._result?.error || null, + convergence_verified: task._result?.convergence_verified || [] + } + } + Write(`${sessionFolder}/.task/${task.id}.json`, JSON.stringify(updated, null, 2)) +}) +``` + +### Step 6.1: Post-Execution Options + +```javascript +if (!autoYes) { + AskUserQuestion({ + questions: [{ + question: `Execution complete: ${completedTasks.size}/${tasks.length} succeeded. Next:`, + header: "Post-Execute", + multiSelect: false, + options: [ + { label: "Retry Failed", description: `Re-execute ${failedTasks.size} failed tasks` }, + { label: "View Events", description: "Display execution-events.md" }, + { label: "Create Issue", description: "Create issue from failed tasks" }, + { label: "Done", description: "End workflow" } + ] + }] + }) +} +``` + +--- + +## Output Structure + +``` +{sessionFolder}/ +├── .task/ # Individual task JSON files (with _execution state after completion) +│ ├── TASK-001.json +│ └── ... +├── execution.md # Execution overview + task table + summary +└── execution-events.md # Chronological event log +``` + +## execution-events.md Event Format + +```markdown +## {timestamp} — {task.id}: {task.title} + +**Type**: {task.type} | **Priority**: {task.priority} +**Status**: IN PROGRESS +**Files**: {task.files[].path} + +### Execution Log +- Read {file} ({lines} lines) +- Applied: {change description} +- ... + +**Status**: COMPLETED / FAILED +**Files Modified**: {list} + +#### Convergence Verification +- [x/] {criterion 1} +- [x/] {criterion 2} +- **Verification**: {command} → PASS/FAIL + +--- +``` diff --git a/.codex/skills/analyze-with-file/SKILL.md b/.codex/skills/analyze-with-file/SKILL.md index a72a938f..cada240f 100644 --- a/.codex/skills/analyze-with-file/SKILL.md +++ b/.codex/skills/analyze-with-file/SKILL.md @@ -109,13 +109,15 @@ Step 4: Synthesis & Conclusion ├─ Update discussion.md with final synthesis └─ Offer options: quick execute / create issue / generate task / export / done -Step 5: Quick Execute (Optional - user selects) - ├─ Convert conclusions.recommendations → .task/TASK-*.json (individual task files with convergence) - ├─ Pre-execution analysis (dependencies, file conflicts, execution order) - ├─ User confirmation - ├─ Direct inline execution (Read/Edit/Write/Grep/Glob/Bash) - ├─ Record events → execution-events.md, update execution.md - └─ Report completion summary +Step 5: Execute (Optional - user selects, routes by complexity) + ├─ Simple (≤2 recs): Direct inline execution → summary in discussion.md + └─ Complex (≥3 recs): EXECUTE.md pipeline + ├─ Enrich recommendations → generate .task/TASK-*.json + ├─ Pre-execution analysis (dependencies, file conflicts, execution order) + ├─ User confirmation + ├─ Direct inline execution (Read/Edit/Write/Grep/Glob/Bash) + ├─ Record events → execution-events.md, update execution.md + └─ Report completion summary ``` ## Configuration @@ -563,15 +565,12 @@ const conclusions = { key_conclusions: [ // Main conclusions { point: '...', evidence: '...', confidence: 'high|medium|low' } ], - recommendations: [ // Actionable recommendations (enriched) + recommendations: [ // Actionable recommendations { - action: '...', // What to do (imperative verb + target) - rationale: '...', // Why this matters + action: '...', // What to do (imperative verb + target) + rationale: '...', // Why this matters priority: 'high|medium|low', - target_files: ['path/to/file.ts'], // From exploration-codebase.json relevant_files - changes: ['specific change per file'], // Concrete modification descriptions - implementation_hints: ['step 1', ...], // Key realization steps - evidence_refs: ['file:line', ...] // Supporting evidence locations + evidence_refs: ['file:line', ...] // Supporting evidence locations } ], open_questions: [...], // Unresolved questions @@ -668,7 +667,7 @@ if (!autoYes) { | Selection | Action | |-----------|--------| -| Quick Execute | Jump to Phase 5 (generate .task/*.json → invoke unified-execute) | +| Quick Execute | Jump to Phase 5 (routes by complexity) | | Create Issue | `Skill(skill="issue:new", args="...")` | | Generate Task | Jump to Phase 5 Step 5.1-5.2 only (generate .task/*.json, no execution) | | Export Report | Copy discussion.md + conclusions.json to user-specified location | @@ -680,148 +679,96 @@ if (!autoYes) { - User offered meaningful next step options - **Complete decision trail** documented and traceable from initial scoping to final conclusions -### Phase 5: Task Generation & Execute (Optional) +### Phase 5: Execute (Optional) -**Objective**: Convert analysis conclusions into `.task/*.json` with rich execution context from ALL Phase 2-4 artifacts, then optionally execute via `unified-execute-with-file`. +**Objective**: Execute analysis recommendations — route by complexity. -**Trigger**: User selects "Quick Execute" or "Generate Task" in Phase 4. In auto mode, triggered only for `moderate`/`complex` recommendations. +**Trigger**: User selects "Quick Execute" in Phase 4. In auto mode, triggered only for `moderate`/`complex` recommendations. -**Key Principle**: Task generation leverages ALL artifacts (exploration-codebase.json + explorations/perspectives + conclusions). Execution delegates to `unified-execute-with-file` — no inline execution engine duplication. +**Routing Logic**: -**Flow**: ``` -conclusions.json + exploration-codebase.json + explorations.json - → .task/*.json (enriched with implementation + files[].changes) - → (optional) unified-execute-with-file +complexity assessment (from Phase 4.3) + ├─ simple/moderate (≤2 recommendations, clear changes) + │ └─ Direct inline execution — no .task/*.json overhead + └─ complex (≥3 recommendations, or high-priority with dependencies) + └─ Route to EXECUTE.md — full pipeline (task generation → execution) ``` -**Quality spec**: See `EXECUTE.md` for task generation standards, file resolution algorithm, and convergence validation. - -**Schema**: `cat ~/.ccw/workflows/cli-templates/schemas/task-schema.json` - -##### Step 5.1: Load All Context Sources +##### Step 5.1: Route by Complexity ```javascript -const conclusions = JSON.parse(Read(`${sessionFolder}/conclusions.json`)) +const recs = conclusions.recommendations || [] -// CRITICAL: Load codebase context for file mapping -const codebaseContext = file_exists(`${sessionFolder}/exploration-codebase.json`) - ? JSON.parse(Read(`${sessionFolder}/exploration-codebase.json`)) - : null - -const explorations = file_exists(`${sessionFolder}/explorations.json`) - ? JSON.parse(Read(`${sessionFolder}/explorations.json`)) - : file_exists(`${sessionFolder}/perspectives.json`) - ? JSON.parse(Read(`${sessionFolder}/perspectives.json`)) - : null -``` - -##### Step 5.2: Generate .task/*.json - -Convert `conclusions.recommendations` into individual task JSON files. Each task MUST include `files[].changes` and `implementation` steps — see EXECUTE.md for quality standards. - -```javascript -const tasks = conclusions.recommendations.map((rec, index) => { - const taskId = `TASK-${String(index + 1).padStart(3, '0')}` - - // File resolution: rec.target_files → codebaseContext → explorations (see EXECUTE.md) - const targetFiles = resolveTargetFiles(rec, codebaseContext, explorations) - - return { - id: taskId, - title: rec.action, - description: rec.rationale, - type: inferTaskType(rec), - priority: rec.priority, - effort: inferEffort(rec), - - // FILES with change details (not just paths) - files: targetFiles.map(f => ({ - path: f.path, - action: f.action || 'modify', - target: f.target || null, // Function/class name to modify - changes: f.changes || [], // Specific change descriptions - change: f.changes?.[0] || rec.action // Primary change description - })), - - depends_on: [], - - // CONVERGENCE (must pass quality validation — see EXECUTE.md) - convergence: { - criteria: generateCriteria(rec), - verification: generateVerification(rec), - definition_of_done: generateDoD(rec) - }, - - // IMPLEMENTATION steps (critical for execution agent) - implementation: (rec.implementation_hints || [rec.action]).map((hint, i) => ({ - step: `${i + 1}`, - description: hint, - actions: rec.changes?.filter(c => c.includes(hint.split(' ')[0])) || [] - })), - - // CONTEXT - evidence: rec.evidence_refs || [], - source: { - tool: 'analyze-with-file', - session_id: sessionId, - original_id: taskId - } - } -}) - -// Quality validation (see EXECUTE.md for rules) -validateConvergenceQuality(tasks) - -// Write each task as individual JSON file -Bash(`mkdir -p ${sessionFolder}/.task`) -tasks.forEach(task => { - Write(`${sessionFolder}/.task/${task.id}.json`, JSON.stringify(task, null, 2)) -}) -``` - -##### Step 5.3: User Confirmation & Execution Delegation - -```javascript -if (!autoYes) { - const action = AskUserQuestion({ - questions: [{ - question: `Generated ${tasks.length} tasks in .task/. Next:`, - header: "Execute", - multiSelect: false, - options: [ - { label: "Execute Now", description: "Invoke unified-execute-with-file on .task/" }, - { label: "Adjust Tasks", description: "Review and modify .task/*.json before execution" }, - { label: "Done", description: "Keep .task/*.json, execute later manually" } - ] - }] - }) - - if (action === 'Execute Now') { - // Delegate execution to unified-execute-with-file - Skill(skill="workflow:unified-execute-with-file", - args=`PLAN="${sessionFolder}/.task/"`) - } - // "Adjust Tasks": user edits .task/*.json, then invokes unified-execute separately - // "Done": display .task/ path, end workflow +if (recs.length >= 3 || recs.some(r => r.priority === 'high')) { + // COMPLEX PATH → EXECUTE.md pipeline + // Full specification: EXECUTE.md + // Flow: load all context → generate .task/*.json → pre-execution analysis → serial execution → finalize } else { - // Auto mode: generate .task/*.json only - // Execution requires separate invocation: - // /codex:unified-execute-with-file PLAN="${sessionFolder}/.task/" + // SIMPLE PATH → direct inline execution (below) } ``` -**Execution Engine**: `unified-execute-with-file` handles the full execution lifecycle: -- Pre-execution analysis (dependency validation, file conflicts, topological sort) -- Serial task execution with convergence verification -- Progress tracking via `execution.md` + `execution-events.md` -- Auto-commit, failure handling, retry logic +##### Step 5.2: Simple Path — Direct Inline Execution + +For simple/moderate recommendations, execute directly without .task/*.json ceremony: + +```javascript +// For each recommendation: +recs.forEach((rec, index) => { + // 1. Locate relevant files from evidence_refs or codebase search + const files = rec.evidence_refs + ?.filter(ref => ref.includes(':')) + .map(ref => ref.split(':')[0]) || [] + + // 2. Read each target file + files.forEach(filePath => Read(filePath)) + + // 3. Apply changes based on rec.action + rec.rationale + // Use Edit (preferred) for modifications, Write for new files + + // 4. Log to discussion.md — append execution summary +}) + +// Append execution summary to discussion.md +appendToDiscussion(` +## Quick Execution Summary + +- **Recommendations executed**: ${recs.length} +- **Completed**: ${getUtc8ISOString()} + +${recs.map((rec, i) => `### ${i+1}. ${rec.action} +- **Status**: completed/failed +- **Rationale**: ${rec.rationale} +- **Evidence**: ${rec.evidence_refs?.join(', ') || 'N/A'} +`).join('\n')} +`) +``` + +**Simple path characteristics**: +- No `.task/*.json` generation +- No `execution.md` / `execution-events.md` +- Execution summary appended directly to `discussion.md` +- Suitable for 1-2 clear, low-risk recommendations + +##### Step 5.3: Complex Path — EXECUTE.md Pipeline + +For complex recommendations, follow the full specification in `EXECUTE.md`: + +1. **Load context sources**: Reuse in-memory artifacts or read from disk +2. **Enrich recommendations**: Resolve target files, generate implementation steps, build convergence criteria +3. **Generate `.task/*.json`**: Individual task files with full execution context +4. **Pre-execution analysis**: Dependency validation, file conflicts, topological sort +5. **User confirmation**: Present task list, allow adjustment +6. **Serial execution**: Execute each task following generated implementation steps +7. **Finalize**: Update task states, write execution artifacts + +**Full specification**: `EXECUTE.md` **Success Criteria**: -- `.task/*.json` generated with: `files[].changes` populated, `implementation` steps present, convergence quality validated -- `exploration-codebase.json` data incorporated into file targeting -- User informed of .task/ location and next step options -- Execution delegated to `unified-execute-with-file` (no inline execution duplication) +- Simple path: recommendations executed, summary in discussion.md +- Complex path: `.task/*.json` generated with quality validation, execution tracked via execution.md + execution-events.md +- Execution route chosen correctly based on complexity assessment ## Output Structure @@ -835,27 +782,19 @@ if (!autoYes) { │ └── ... ├── explorations.json # Phase 2: Single perspective aggregated findings ├── perspectives.json # Phase 2: Multi-perspective findings with synthesis -├── conclusions.json # Phase 4: Final synthesis with recommendations -└── .task/ # Phase 5: Individual task JSON files (if quick execute / generate task) - ├── TASK-001.json # One file per task with convergence + implementation + source - ├── TASK-002.json - └── ... - -# Execution artifacts (generated by unified-execute-with-file, separate location): -{projectRoot}/.workflow/.execution/EXEC-{slug}-{date}-{random}/ -├── execution.md # Execution overview + task table + summary -└── execution-events.md # Chronological event log +└── conclusions.json # Phase 4: Final synthesis with recommendations ``` +> **Phase 5 complex path** adds `.task/`, `execution.md`, `execution-events.md` — see `EXECUTE.md` for structure. + | File | Phase | Description | |------|-------|-------------| -| `discussion.md` | 1 | Initialized with session metadata, finalized in Phase 4 | +| `discussion.md` | 1-4 | Session metadata → discussion timeline → conclusions. Simple execution summary appended here. | | `exploration-codebase.json` | 2 | Codebase context: relevant files, patterns, constraints | | `explorations/*.json` | 2 | Per-perspective exploration results (multi only) | | `explorations.json` | 2 | Single perspective aggregated findings | | `perspectives.json` | 2 | Multi-perspective findings with cross-perspective synthesis | -| `conclusions.json` | 4 | Final synthesis: conclusions, recommendations (enriched), open questions | -| `.task/*.json` | 5 | Individual task files with convergence + `implementation` + `files[].changes` + source | +| `conclusions.json` | 4 | Final synthesis: conclusions, recommendations, open questions | ## Analysis Dimensions Reference @@ -1004,10 +943,10 @@ Remaining questions or areas for investigation | User timeout in discussion | Save state, show resume command | Use `--continue` to resume | | Max rounds reached (5) | Force synthesis phase | Highlight remaining questions in conclusions | | Session folder conflict | Append timestamp suffix | Create unique folder and continue | -| Quick execute: task fails | Record failure in execution-events.md | User can retry, skip, or abort | -| Quick execute: verification fails | Mark criterion as unverified, continue | Note in events, manual check | +| Quick execute: task fails | Record failure, ask user | Retry, skip, or abort (see EXECUTE.md) | +| Quick execute: verification fails | Mark as unverified | Note in events, manual check | | Quick execute: no recommendations | Cannot generate .task/*.json | Inform user, suggest lite-plan | -| Quick execute: simple recommendations | Complexity too low for .task/*.json | Skip task generation, output conclusions only | +| Quick execute: simple recommendations | Complexity too low for .task/*.json | Direct inline execution (no task generation) | ## Best Practices @@ -1052,9 +991,8 @@ Remaining questions or areas for investigation **Use Quick Execute (Phase 5) when:** - Analysis conclusions contain clear, actionable recommendations -- Context is already sufficient — no additional exploration needed -- Want a streamlined analyze → .task/*.json plan → direct execute pipeline -- Tasks are relatively independent and can be executed serially +- Simple: 1-2 clear changes → direct inline execution (no .task/ overhead) +- Complex: 3+ recommendations with dependencies → EXECUTE.md pipeline (.task/*.json → serial execution) **Consider alternatives when:** - Specific bug diagnosis needed → use `debug-with-file` diff --git a/ccw/frontend/src/App.tsx b/ccw/frontend/src/App.tsx index 32f0d16f..6ea8f6ab 100644 --- a/ccw/frontend/src/App.tsx +++ b/ccw/frontend/src/App.tsx @@ -14,6 +14,7 @@ import type { Locale } from './lib/i18n'; import { useWorkflowStore } from '@/stores/workflowStore'; import { useActiveCliExecutions } from '@/hooks/useActiveCliExecutions'; import { DialogStyleProvider } from '@/contexts/DialogStyleContext'; +import { initializeCsrfToken } from './lib/api'; interface AppProps { locale: Locale; @@ -25,6 +26,11 @@ interface AppProps { * Provides routing and global providers */ function App({ locale, messages }: AppProps) { + // Initialize CSRF token on app mount + useEffect(() => { + initializeCsrfToken().catch(console.error); + }, []); + return ( diff --git a/ccw/frontend/src/components/shared/SkillCard.tsx b/ccw/frontend/src/components/shared/SkillCard.tsx index 75570a02..2460a96e 100644 --- a/ccw/frontend/src/components/shared/SkillCard.tsx +++ b/ccw/frontend/src/components/shared/SkillCard.tsx @@ -12,7 +12,6 @@ import { Settings, Power, PowerOff, - Tag, User, } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -140,64 +139,93 @@ export function SkillCard({ - {/* Header */} -
-
+ {/* Header - Icon, Title, Version on left; Source Badge, Enable Button, Actions Menu on right */} +
+
+ {/* Icon */}
+ + {/* Title and Version */}
-

{skill.name}

+

{skill.name}

{skill.version && (

v{skill.version}

)}
- {showActions && ( - - - - - - onClick?.(skill)}> - - {formatMessage({ id: 'skills.actions.viewDetails' })} - - - - {formatMessage({ id: 'skills.actions.configure' })} - - - {skill.enabled ? ( - <> - - {formatMessage({ id: 'skills.actions.disable' })} - - ) : ( - <> - - {formatMessage({ id: 'skills.actions.enable' })} - - )} - - - - )} + + {/* Right side: Source Badge, Enable Icon Button, Actions Menu */} +
+ + + {showActions && ( + + + + + + onClick?.(skill)}> + + {formatMessage({ id: 'skills.actions.viewDetails' })} + + + + {formatMessage({ id: 'skills.actions.configure' })} + + + {skill.enabled ? ( + <> + + {formatMessage({ id: 'skills.actions.disable' })} + + ) : ( + <> + + {formatMessage({ id: 'skills.actions.enable' })} + + )} + + + + )} +
{/* Description */} @@ -205,65 +233,35 @@ export function SkillCard({ {skill.description}

- {/* Triggers */} - {skill.triggers && skill.triggers.length > 0 && ( -
-
- - {formatMessage({ id: 'skills.card.triggers' })} -
-
- {skill.triggers.slice(0, 4).map((trigger) => ( + {/* Footer - Tags, Category, Author */} +
+ {/* Tags (first 2 triggers) */} + {skill.triggers && skill.triggers.length > 0 && ( + <> + {skill.triggers.slice(0, 2).map((trigger) => ( {trigger} ))} - {skill.triggers.length > 4 && ( + {skill.triggers.length > 2 && ( - +{skill.triggers.length - 4} + +{skill.triggers.length - 2} )} + + )} + {skill.category && ( + + {skill.category} + + )} + {skill.author && ( +
+ + {skill.author}
-
- )} - - {/* Footer */} -
-
- - {skill.category && ( - - {skill.category} - - )} -
- + )}
- - {/* Author */} - {skill.author && ( -
- - {skill.author} -
- )} ); } diff --git a/ccw/frontend/src/lib/api.ts b/ccw/frontend/src/lib/api.ts index f49cf7e3..0853fa7d 100644 --- a/ccw/frontend/src/lib/api.ts +++ b/ccw/frontend/src/lib/api.ts @@ -104,11 +104,42 @@ export interface ApiError { // ========== CSRF Token Handling ========== /** - * Get CSRF token from cookie + * In-memory CSRF token storage + * The token is obtained from X-CSRF-Token response header and stored here + * because the XSRF-TOKEN cookie is HttpOnly and cannot be read by JavaScript + */ +let csrfToken: string | null = null; + +/** + * Get CSRF token from memory */ function getCsrfToken(): string | null { - const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/); - return match ? decodeURIComponent(match[1]) : null; + return csrfToken; +} + +/** + * Set CSRF token from response header + */ +function updateCsrfToken(response: Response): void { + const token = response.headers.get('X-CSRF-Token'); + if (token) { + csrfToken = token; + } +} + +/** + * Initialize CSRF token by fetching from server + * Should be called once on app initialization + */ +export async function initializeCsrfToken(): Promise { + try { + const response = await fetch('/api/csrf-token', { + credentials: 'same-origin', + }); + updateCsrfToken(response); + } catch (error) { + console.error('[CSRF] Failed to initialize CSRF token:', error); + } } // ========== Base Fetch Wrapper ========== @@ -124,9 +155,9 @@ async function fetchApi( // Add CSRF token for mutating requests if (options.method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) { - const csrfToken = getCsrfToken(); - if (csrfToken) { - headers.set('X-CSRF-Token', csrfToken); + const token = getCsrfToken(); + if (token) { + headers.set('X-CSRF-Token', token); } } @@ -141,6 +172,9 @@ async function fetchApi( credentials: 'same-origin', }); + // Update CSRF token from response header + updateCsrfToken(response); + if (!response.ok) { const error: ApiError = { message: response.statusText || 'Request failed', diff --git a/ccw/frontend/src/pages/SkillsManagerPage.tsx b/ccw/frontend/src/pages/SkillsManagerPage.tsx index 13ecefdc..d583e177 100644 --- a/ccw/frontend/src/pages/SkillsManagerPage.tsx +++ b/ccw/frontend/src/pages/SkillsManagerPage.tsx @@ -429,7 +429,6 @@ export function SkillsManagerPage() { value: 'hub', label: formatMessage({ id: 'skills.location.hub' }), icon: , - badge: {hubStats.data?.installedTotal || 0}, disabled: isToggling, }, ]} diff --git a/ccw/src/core/routes/skill-hub-routes.ts b/ccw/src/core/routes/skill-hub-routes.ts index 98879603..a9ac7299 100644 --- a/ccw/src/core/routes/skill-hub-routes.ts +++ b/ccw/src/core/routes/skill-hub-routes.ts @@ -318,17 +318,34 @@ async function fetchRemoteSkillIndex(): Promise { // Check cache const now = Date.now(); if (remoteSkillsCache.data && (now - remoteSkillsCache.timestamp) < CACHE_TTL_MS) { + console.log('[SkillHub] Using cached remote index'); return remoteSkillsCache.data; } const indexUrl = `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.skillIndexPath}`; + console.log('[SkillHub] Fetching remote index from:', indexUrl); try { - const response = await fetch(indexUrl); + // Add timeout to prevent hanging + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout + + const response = await fetch(indexUrl, { + signal: controller.signal, + headers: { + 'User-Agent': 'CCW-SkillHub/1.0' + } + }); + clearTimeout(timeoutId); + + console.log('[SkillHub] Fetch response status:', response.status, response.statusText); + if (!response.ok) { // Try local fallback + console.log('[SkillHub] Fetch failed, trying local fallback'); const localIndex = loadLocalIndex(); if (localIndex) { + console.log('[SkillHub] Using local fallback index'); return localIndex; } throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); @@ -336,6 +353,7 @@ async function fetchRemoteSkillIndex(): Promise { const index = await response.json() as RemoteSkillIndex; index.source = 'github'; + console.log('[SkillHub] Successfully fetched remote index with', index.skills.length, 'skills'); // Update cache remoteSkillsCache = { data: index, timestamp: now }; @@ -345,17 +363,27 @@ async function fetchRemoteSkillIndex(): Promise { return index; } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + console.error('[SkillHub] Error fetching remote index:', errorMsg); + // Return cached data if available, even if expired if (remoteSkillsCache.data) { + console.log('[SkillHub] Using expired cache as fallback'); return remoteSkillsCache.data; } // Try local fallback const localIndex = loadLocalIndex(); if (localIndex) { + console.log('[SkillHub] Using local fallback index after error'); return localIndex; } + // If it's a timeout or network error, provide a more helpful message + if (errorMsg.includes('aborted') || errorMsg.includes('timeout')) { + throw new Error('Network timeout - please check your internet connection'); + } + throw error; } } @@ -395,11 +423,35 @@ function saveCachedIndex(index: RemoteSkillIndex): void { * Fetch a single skill from remote URL */ async function fetchRemoteSkill(downloadUrl: string): Promise { - const response = await fetch(downloadUrl); - if (!response.ok) { - throw new Error(`Failed to fetch skill: ${response.status} ${response.statusText}`); + console.log('[SkillHub] Fetching skill from:', downloadUrl); + + try { + // Add timeout to prevent hanging + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout + + const response = await fetch(downloadUrl, { + signal: controller.signal, + headers: { + 'User-Agent': 'CCW-SkillHub/1.0' + } + }); + clearTimeout(timeoutId); + + console.log('[SkillHub] Fetch skill response status:', response.status, response.statusText); + + if (!response.ok) { + throw new Error(`Failed to fetch skill: ${response.status} ${response.statusText}`); + } + + const content = await response.text(); + console.log('[SkillHub] Successfully fetched skill, size:', content.length, 'bytes'); + return content; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + console.error('[SkillHub] Error fetching skill:', errorMsg); + throw error; } - return response.text(); } /**