mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 14:03:54 +08:00
refactor(req-plan): streamline codebase exploration and decomposition guidelines
This commit is contained in:
@@ -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.
|
> **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
|
## 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 ──┐
|
Step 1: Load context → Step 2: Generate .task/*.json → Step 3: Pre-execution analysis
|
||||||
├── resolveTargetFiles() ──┐
|
→ Step 4: User confirmation → Step 5: Serial execution → Step 6: Finalize
|
||||||
codebaseContext ───┘ ├── .task/TASK-001.json
|
|
||||||
├── .task/TASK-002.json
|
|
||||||
explorations.json ── enrichContext() ─────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Input artifacts** (all from session folder):
|
**Input artifacts** (all from session folder):
|
||||||
|
|
||||||
| Artifact | Required | Provides |
|
| 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 |
|
| `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 |
|
| `explorations.json` | No | `sources[]`, `key_findings[]` — fallback for file resolution |
|
||||||
| `perspectives.json` | No | Multi-perspective findings — alternative to explorations.json |
|
| `perspectives.json` | No | Multi-perspective findings — alternative to explorations.json |
|
||||||
@@ -29,19 +28,26 @@ explorations.json ── enrichContext() ─────────┘
|
|||||||
|
|
||||||
## File Resolution Algorithm
|
## 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
|
```javascript
|
||||||
function resolveTargetFiles(rec, codebaseContext, explorations) {
|
function resolveTargetFiles(rec, codebaseContext, explorations) {
|
||||||
// Priority 1: Explicit target_files from recommendations (Phase 4 enriched)
|
// Priority 1: Extract file paths from evidence_refs (e.g., "src/auth/token.ts:89")
|
||||||
if (rec.target_files?.length) {
|
if (rec.evidence_refs?.length) {
|
||||||
return rec.target_files.map(path => ({
|
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,
|
path,
|
||||||
action: 'modify',
|
action: 'modify',
|
||||||
target: null,
|
target: null,
|
||||||
changes: rec.changes || []
|
changes: []
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Priority 2: Match from exploration-codebase.json relevant_files
|
// Priority 2: Match from exploration-codebase.json relevant_files
|
||||||
if (codebaseContext?.relevant_files?.length) {
|
if (codebaseContext?.relevant_files?.length) {
|
||||||
@@ -133,8 +139,8 @@ function inferTaskType(rec) {
|
|||||||
| priority=low OR single file | `small` |
|
| priority=low OR single file | `small` |
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function inferEffort(rec) {
|
function inferEffort(rec, targetFiles) {
|
||||||
const fileCount = rec.target_files?.length || 0
|
const fileCount = targetFiles?.length || 0
|
||||||
if (rec.priority === 'high' && fileCount >= 3) return 'large'
|
if (rec.priority === 'high' && fileCount >= 3) return 'large'
|
||||||
if (rec.priority === 'high' || fileCount >= 2) return 'medium'
|
if (rec.priority === 'high' || fileCount >= 2) return 'medium'
|
||||||
if (rec.priority === 'low' || fileCount <= 1) return 'small'
|
if (rec.priority === 'low' || fileCount <= 1) return 'small'
|
||||||
@@ -252,7 +258,7 @@ function validateConvergenceQuality(tasks) {
|
|||||||
|
|
||||||
## Required Task Fields (analyze-with-file producer)
|
## 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 |
|
| 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
|
```javascript
|
||||||
/codex:unified-execute-with-file PLAN="${sessionFolder}/.task/"
|
// 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:
|
## Step 2: Enrich Recommendations & Generate .task/*.json
|
||||||
- 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
|
|
||||||
|
|
||||||
**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
|
||||||
|
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|||||||
@@ -109,8 +109,10 @@ Step 4: Synthesis & Conclusion
|
|||||||
├─ Update discussion.md with final synthesis
|
├─ Update discussion.md with final synthesis
|
||||||
└─ Offer options: quick execute / create issue / generate task / export / done
|
└─ Offer options: quick execute / create issue / generate task / export / done
|
||||||
|
|
||||||
Step 5: Quick Execute (Optional - user selects)
|
Step 5: Execute (Optional - user selects, routes by complexity)
|
||||||
├─ Convert conclusions.recommendations → .task/TASK-*.json (individual task files with convergence)
|
├─ 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)
|
├─ Pre-execution analysis (dependencies, file conflicts, execution order)
|
||||||
├─ User confirmation
|
├─ User confirmation
|
||||||
├─ Direct inline execution (Read/Edit/Write/Grep/Glob/Bash)
|
├─ Direct inline execution (Read/Edit/Write/Grep/Glob/Bash)
|
||||||
@@ -563,14 +565,11 @@ const conclusions = {
|
|||||||
key_conclusions: [ // Main conclusions
|
key_conclusions: [ // Main conclusions
|
||||||
{ point: '...', evidence: '...', confidence: 'high|medium|low' }
|
{ point: '...', evidence: '...', confidence: 'high|medium|low' }
|
||||||
],
|
],
|
||||||
recommendations: [ // Actionable recommendations (enriched)
|
recommendations: [ // Actionable recommendations
|
||||||
{
|
{
|
||||||
action: '...', // What to do (imperative verb + target)
|
action: '...', // What to do (imperative verb + target)
|
||||||
rationale: '...', // Why this matters
|
rationale: '...', // Why this matters
|
||||||
priority: 'high|medium|low',
|
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
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -668,7 +667,7 @@ if (!autoYes) {
|
|||||||
|
|
||||||
| Selection | Action |
|
| 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="...")` |
|
| Create Issue | `Skill(skill="issue:new", args="...")` |
|
||||||
| Generate Task | Jump to Phase 5 Step 5.1-5.2 only (generate .task/*.json, no execution) |
|
| 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 |
|
| Export Report | Copy discussion.md + conclusions.json to user-specified location |
|
||||||
@@ -680,148 +679,96 @@ if (!autoYes) {
|
|||||||
- User offered meaningful next step options
|
- User offered meaningful next step options
|
||||||
- **Complete decision trail** documented and traceable from initial scoping to final conclusions
|
- **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
|
complexity assessment (from Phase 4.3)
|
||||||
→ .task/*.json (enriched with implementation + files[].changes)
|
├─ simple/moderate (≤2 recommendations, clear changes)
|
||||||
→ (optional) unified-execute-with-file
|
│ └─ 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.
|
##### Step 5.1: Route by Complexity
|
||||||
|
|
||||||
**Schema**: `cat ~/.ccw/workflows/cli-templates/schemas/task-schema.json`
|
|
||||||
|
|
||||||
##### Step 5.1: Load All Context Sources
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const conclusions = JSON.parse(Read(`${sessionFolder}/conclusions.json`))
|
const recs = conclusions.recommendations || []
|
||||||
|
|
||||||
// CRITICAL: Load codebase context for file mapping
|
if (recs.length >= 3 || recs.some(r => r.priority === 'high')) {
|
||||||
const codebaseContext = file_exists(`${sessionFolder}/exploration-codebase.json`)
|
// COMPLEX PATH → EXECUTE.md pipeline
|
||||||
? JSON.parse(Read(`${sessionFolder}/exploration-codebase.json`))
|
// Full specification: EXECUTE.md
|
||||||
: null
|
// Flow: load all context → generate .task/*.json → pre-execution analysis → serial execution → finalize
|
||||||
|
|
||||||
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
|
|
||||||
} else {
|
} else {
|
||||||
// Auto mode: generate .task/*.json only
|
// SIMPLE PATH → direct inline execution (below)
|
||||||
// Execution requires separate invocation:
|
|
||||||
// /codex:unified-execute-with-file PLAN="${sessionFolder}/.task/"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Execution Engine**: `unified-execute-with-file` handles the full execution lifecycle:
|
##### Step 5.2: Simple Path — Direct Inline Execution
|
||||||
- Pre-execution analysis (dependency validation, file conflicts, topological sort)
|
|
||||||
- Serial task execution with convergence verification
|
For simple/moderate recommendations, execute directly without .task/*.json ceremony:
|
||||||
- Progress tracking via `execution.md` + `execution-events.md`
|
|
||||||
- Auto-commit, failure handling, retry logic
|
```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**:
|
**Success Criteria**:
|
||||||
- `.task/*.json` generated with: `files[].changes` populated, `implementation` steps present, convergence quality validated
|
- Simple path: recommendations executed, summary in discussion.md
|
||||||
- `exploration-codebase.json` data incorporated into file targeting
|
- Complex path: `.task/*.json` generated with quality validation, execution tracked via execution.md + execution-events.md
|
||||||
- User informed of .task/ location and next step options
|
- Execution route chosen correctly based on complexity assessment
|
||||||
- Execution delegated to `unified-execute-with-file` (no inline execution duplication)
|
|
||||||
|
|
||||||
## Output Structure
|
## Output Structure
|
||||||
|
|
||||||
@@ -835,27 +782,19 @@ if (!autoYes) {
|
|||||||
│ └── ...
|
│ └── ...
|
||||||
├── explorations.json # Phase 2: Single perspective aggregated findings
|
├── explorations.json # Phase 2: Single perspective aggregated findings
|
||||||
├── perspectives.json # Phase 2: Multi-perspective findings with synthesis
|
├── perspectives.json # Phase 2: Multi-perspective findings with synthesis
|
||||||
├── conclusions.json # Phase 4: Final synthesis with recommendations
|
└── 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Phase 5 complex path** adds `.task/`, `execution.md`, `execution-events.md` — see `EXECUTE.md` for structure.
|
||||||
|
|
||||||
| File | Phase | Description |
|
| 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 |
|
| `exploration-codebase.json` | 2 | Codebase context: relevant files, patterns, constraints |
|
||||||
| `explorations/*.json` | 2 | Per-perspective exploration results (multi only) |
|
| `explorations/*.json` | 2 | Per-perspective exploration results (multi only) |
|
||||||
| `explorations.json` | 2 | Single perspective aggregated findings |
|
| `explorations.json` | 2 | Single perspective aggregated findings |
|
||||||
| `perspectives.json` | 2 | Multi-perspective findings with cross-perspective synthesis |
|
| `perspectives.json` | 2 | Multi-perspective findings with cross-perspective synthesis |
|
||||||
| `conclusions.json` | 4 | Final synthesis: conclusions, recommendations (enriched), open questions |
|
| `conclusions.json` | 4 | Final synthesis: conclusions, recommendations, open questions |
|
||||||
| `.task/*.json` | 5 | Individual task files with convergence + `implementation` + `files[].changes` + source |
|
|
||||||
|
|
||||||
## Analysis Dimensions Reference
|
## 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 |
|
| 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 |
|
| Max rounds reached (5) | Force synthesis phase | Highlight remaining questions in conclusions |
|
||||||
| Session folder conflict | Append timestamp suffix | Create unique folder and continue |
|
| 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: task fails | Record failure, ask user | Retry, skip, or abort (see EXECUTE.md) |
|
||||||
| Quick execute: verification fails | Mark criterion as unverified, continue | Note in events, manual check |
|
| 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: 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
|
## Best Practices
|
||||||
|
|
||||||
@@ -1052,9 +991,8 @@ Remaining questions or areas for investigation
|
|||||||
|
|
||||||
**Use Quick Execute (Phase 5) when:**
|
**Use Quick Execute (Phase 5) when:**
|
||||||
- Analysis conclusions contain clear, actionable recommendations
|
- Analysis conclusions contain clear, actionable recommendations
|
||||||
- Context is already sufficient — no additional exploration needed
|
- Simple: 1-2 clear changes → direct inline execution (no .task/ overhead)
|
||||||
- Want a streamlined analyze → .task/*.json plan → direct execute pipeline
|
- Complex: 3+ recommendations with dependencies → EXECUTE.md pipeline (.task/*.json → serial execution)
|
||||||
- Tasks are relatively independent and can be executed serially
|
|
||||||
|
|
||||||
**Consider alternatives when:**
|
**Consider alternatives when:**
|
||||||
- Specific bug diagnosis needed → use `debug-with-file`
|
- Specific bug diagnosis needed → use `debug-with-file`
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type { Locale } from './lib/i18n';
|
|||||||
import { useWorkflowStore } from '@/stores/workflowStore';
|
import { useWorkflowStore } from '@/stores/workflowStore';
|
||||||
import { useActiveCliExecutions } from '@/hooks/useActiveCliExecutions';
|
import { useActiveCliExecutions } from '@/hooks/useActiveCliExecutions';
|
||||||
import { DialogStyleProvider } from '@/contexts/DialogStyleContext';
|
import { DialogStyleProvider } from '@/contexts/DialogStyleContext';
|
||||||
|
import { initializeCsrfToken } from './lib/api';
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
@@ -25,6 +26,11 @@ interface AppProps {
|
|||||||
* Provides routing and global providers
|
* Provides routing and global providers
|
||||||
*/
|
*/
|
||||||
function App({ locale, messages }: AppProps) {
|
function App({ locale, messages }: AppProps) {
|
||||||
|
// Initialize CSRF token on app mount
|
||||||
|
useEffect(() => {
|
||||||
|
initializeCsrfToken().catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
<IntlProvider locale={locale} messages={messages}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
Power,
|
Power,
|
||||||
PowerOff,
|
PowerOff,
|
||||||
Tag,
|
|
||||||
User,
|
User,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -140,34 +139,62 @@ export function SkillCard({
|
|||||||
<Card
|
<Card
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-4 cursor-pointer hover:shadow-md transition-all hover-glow',
|
'p-4 cursor-pointer transition-all hover-glow',
|
||||||
skill.enabled ? 'hover:border-primary/50' : 'border-dashed border-muted-foreground/50 bg-muted/30 grayscale-[0.3]',
|
skill.enabled
|
||||||
|
? 'border-primary/20 bg-primary/[0.02] hover:border-primary/40 hover:shadow-md'
|
||||||
|
: 'border-border/50 hover:border-border hover:shadow-sm',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header - Icon, Title, Version on left; Source Badge, Enable Button, Actions Menu on right */}
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex items-start gap-3 min-w-0">
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
|
{/* Icon */}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'p-2 rounded-lg flex-shrink-0',
|
'p-2 rounded-lg flex-shrink-0',
|
||||||
skill.enabled ? 'bg-primary/10' : 'bg-muted'
|
skill.enabled ? 'bg-primary/10' : 'bg-muted'
|
||||||
)}>
|
)}>
|
||||||
<Sparkles className={cn('w-5 h-5', skill.enabled ? 'text-primary' : 'text-muted-foreground')} />
|
<Sparkles className={cn('w-5 h-5', skill.enabled ? 'text-primary' : 'text-muted-foreground')} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Title and Version */}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h3 className="text-sm font-medium text-foreground">{skill.name}</h3>
|
<h3 className="text-sm font-medium text-foreground truncate">{skill.name}</h3>
|
||||||
{skill.version && (
|
{skill.version && (
|
||||||
<p className="text-xs text-muted-foreground">v{skill.version}</p>
|
<p className="text-xs text-muted-foreground">v{skill.version}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Right side: Source Badge, Enable Icon Button, Actions Menu */}
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
<SourceBadge source={skill.source} />
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
"h-8 w-8 p-0",
|
||||||
|
skill.enabled
|
||||||
|
? "bg-primary hover:bg-primary/90"
|
||||||
|
: "hover:bg-muted"
|
||||||
|
)}
|
||||||
|
onClick={handleToggle}
|
||||||
|
disabled={isToggling}
|
||||||
|
title={skill.enabled ? formatMessage({ id: 'skills.state.enabled' }) : formatMessage({ id: 'skills.state.disabled' })}
|
||||||
|
>
|
||||||
|
{skill.enabled ? (
|
||||||
|
<Power className="w-4 h-4 text-white" />
|
||||||
|
) : (
|
||||||
|
<PowerOff className="w-4 h-4 text-foreground" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
{showActions && (
|
{showActions && (
|
||||||
<DropdownMenu open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
<DropdownMenu open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0 flex-shrink-0"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreVertical className="w-4 h-4" />
|
<MoreVertical className="w-4 h-4" />
|
||||||
@@ -199,71 +226,42 @@ export function SkillCard({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<p className="text-sm text-muted-foreground mt-3 line-clamp-2">
|
<p className="text-sm text-muted-foreground mt-3 line-clamp-2">
|
||||||
{skill.description}
|
{skill.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Triggers */}
|
{/* Footer - Tags, Category, Author */}
|
||||||
|
<div className="flex items-center gap-2 mt-4 pt-3 border-t border-border flex-wrap">
|
||||||
|
{/* Tags (first 2 triggers) */}
|
||||||
{skill.triggers && skill.triggers.length > 0 && (
|
{skill.triggers && skill.triggers.length > 0 && (
|
||||||
<div className="mt-3">
|
<>
|
||||||
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
|
{skill.triggers.slice(0, 2).map((trigger) => (
|
||||||
<Tag className="w-3 h-3" />
|
|
||||||
{formatMessage({ id: 'skills.card.triggers' })}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{skill.triggers.slice(0, 4).map((trigger) => (
|
|
||||||
<Badge key={trigger} variant="outline" className="text-xs">
|
<Badge key={trigger} variant="outline" className="text-xs">
|
||||||
{trigger}
|
{trigger}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
{skill.triggers.length > 4 && (
|
{skill.triggers.length > 2 && (
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
+{skill.triggers.length - 4}
|
+{skill.triggers.length - 2}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="flex items-center justify-between mt-4 pt-3 border-t border-border">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<SourceBadge source={skill.source} />
|
|
||||||
{skill.category && (
|
{skill.category && (
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{skill.category}
|
{skill.category}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant={skill.enabled ? 'default' : 'outline'}
|
|
||||||
size="sm"
|
|
||||||
onClick={handleToggle}
|
|
||||||
disabled={isToggling}
|
|
||||||
>
|
|
||||||
{skill.enabled ? (
|
|
||||||
<>
|
|
||||||
<Power className="w-4 h-4 mr-1" />
|
|
||||||
{formatMessage({ id: 'skills.state.enabled' })}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<PowerOff className="w-4 h-4 mr-1" />
|
|
||||||
{formatMessage({ id: 'skills.state.disabled' })}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Author */}
|
|
||||||
{skill.author && (
|
{skill.author && (
|
||||||
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
<User className="w-3 h-3" />
|
<User className="w-3 h-3" />
|
||||||
{skill.author}
|
{skill.author}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,11 +104,42 @@ export interface ApiError {
|
|||||||
// ========== CSRF Token Handling ==========
|
// ========== 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 {
|
function getCsrfToken(): string | null {
|
||||||
const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);
|
return csrfToken;
|
||||||
return match ? decodeURIComponent(match[1]) : null;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<void> {
|
||||||
|
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 ==========
|
// ========== Base Fetch Wrapper ==========
|
||||||
@@ -124,9 +155,9 @@ async function fetchApi<T>(
|
|||||||
|
|
||||||
// Add CSRF token for mutating requests
|
// Add CSRF token for mutating requests
|
||||||
if (options.method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) {
|
if (options.method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) {
|
||||||
const csrfToken = getCsrfToken();
|
const token = getCsrfToken();
|
||||||
if (csrfToken) {
|
if (token) {
|
||||||
headers.set('X-CSRF-Token', csrfToken);
|
headers.set('X-CSRF-Token', token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +172,9 @@ async function fetchApi<T>(
|
|||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update CSRF token from response header
|
||||||
|
updateCsrfToken(response);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error: ApiError = {
|
const error: ApiError = {
|
||||||
message: response.statusText || 'Request failed',
|
message: response.statusText || 'Request failed',
|
||||||
|
|||||||
@@ -429,7 +429,6 @@ export function SkillsManagerPage() {
|
|||||||
value: 'hub',
|
value: 'hub',
|
||||||
label: formatMessage({ id: 'skills.location.hub' }),
|
label: formatMessage({ id: 'skills.location.hub' }),
|
||||||
icon: <Globe className="h-4 w-4" />,
|
icon: <Globe className="h-4 w-4" />,
|
||||||
badge: <Badge variant="secondary" className="ml-2">{hubStats.data?.installedTotal || 0}</Badge>,
|
|
||||||
disabled: isToggling,
|
disabled: isToggling,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -318,17 +318,34 @@ async function fetchRemoteSkillIndex(): Promise<RemoteSkillIndex> {
|
|||||||
// Check cache
|
// Check cache
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (remoteSkillsCache.data && (now - remoteSkillsCache.timestamp) < CACHE_TTL_MS) {
|
if (remoteSkillsCache.data && (now - remoteSkillsCache.timestamp) < CACHE_TTL_MS) {
|
||||||
|
console.log('[SkillHub] Using cached remote index');
|
||||||
return remoteSkillsCache.data;
|
return remoteSkillsCache.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexUrl = `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.skillIndexPath}`;
|
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 {
|
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) {
|
if (!response.ok) {
|
||||||
// Try local fallback
|
// Try local fallback
|
||||||
|
console.log('[SkillHub] Fetch failed, trying local fallback');
|
||||||
const localIndex = loadLocalIndex();
|
const localIndex = loadLocalIndex();
|
||||||
if (localIndex) {
|
if (localIndex) {
|
||||||
|
console.log('[SkillHub] Using local fallback index');
|
||||||
return localIndex;
|
return localIndex;
|
||||||
}
|
}
|
||||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||||
@@ -336,6 +353,7 @@ async function fetchRemoteSkillIndex(): Promise<RemoteSkillIndex> {
|
|||||||
|
|
||||||
const index = await response.json() as RemoteSkillIndex;
|
const index = await response.json() as RemoteSkillIndex;
|
||||||
index.source = 'github';
|
index.source = 'github';
|
||||||
|
console.log('[SkillHub] Successfully fetched remote index with', index.skills.length, 'skills');
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
remoteSkillsCache = { data: index, timestamp: now };
|
remoteSkillsCache = { data: index, timestamp: now };
|
||||||
@@ -345,17 +363,27 @@ async function fetchRemoteSkillIndex(): Promise<RemoteSkillIndex> {
|
|||||||
|
|
||||||
return index;
|
return index;
|
||||||
} catch (error) {
|
} 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
|
// Return cached data if available, even if expired
|
||||||
if (remoteSkillsCache.data) {
|
if (remoteSkillsCache.data) {
|
||||||
|
console.log('[SkillHub] Using expired cache as fallback');
|
||||||
return remoteSkillsCache.data;
|
return remoteSkillsCache.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try local fallback
|
// Try local fallback
|
||||||
const localIndex = loadLocalIndex();
|
const localIndex = loadLocalIndex();
|
||||||
if (localIndex) {
|
if (localIndex) {
|
||||||
|
console.log('[SkillHub] Using local fallback index after error');
|
||||||
return localIndex;
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,11 +423,35 @@ function saveCachedIndex(index: RemoteSkillIndex): void {
|
|||||||
* Fetch a single skill from remote URL
|
* Fetch a single skill from remote URL
|
||||||
*/
|
*/
|
||||||
async function fetchRemoteSkill(downloadUrl: string): Promise<string> {
|
async function fetchRemoteSkill(downloadUrl: string): Promise<string> {
|
||||||
const response = await fetch(downloadUrl);
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch skill: ${response.status} ${response.statusText}`);
|
throw new Error(`Failed to fetch skill: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
return response.text();
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user