mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35ffd3419e | ||
|
|
e3223edbb1 | ||
|
|
a061fc1428 | ||
|
|
0992d27523 | ||
|
|
5aa0c9610d | ||
|
|
7620ff703d |
@@ -26,7 +26,7 @@ color: green
|
||||
- Dependency DAG validation
|
||||
- Auto-bind for single solution, return for selection on multiple
|
||||
|
||||
**Key Principle**: Generate tasks conforming to schema with quantified delivery_criteria.
|
||||
**Key Principle**: Generate tasks conforming to schema with quantified acceptance criteria.
|
||||
|
||||
---
|
||||
|
||||
@@ -71,11 +71,17 @@ function analyzeIssue(issue) {
|
||||
issue_id: issue.id,
|
||||
requirements: extractRequirements(issue.description),
|
||||
scope: inferScope(issue.title, issue.description),
|
||||
complexity: determineComplexity(issue) // Low | Medium | High
|
||||
complexity: determineComplexity(issue), // Low | Medium | High
|
||||
lifecycle: issue.lifecycle_requirements // User preferences for test/commit
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3**: Apply lifecycle requirements to tasks
|
||||
- `lifecycle.test_strategy` → Configure `test.unit`, `test.commands`
|
||||
- `lifecycle.commit_strategy` → Configure `commit.type`, `commit.scope`
|
||||
- `lifecycle.regression_scope` → Configure `regression` array
|
||||
|
||||
**Complexity Rules**:
|
||||
| Complexity | Files | Tasks |
|
||||
|------------|-------|-------|
|
||||
@@ -100,26 +106,76 @@ mcp__ace-tool__search_context({
|
||||
- [ ] Discover dependencies
|
||||
- [ ] Locate test patterns
|
||||
|
||||
**Fallback**: ACE → ripgrep → Glob
|
||||
**Fallback Chain**: ACE → smart_search → Grep → rg → Glob
|
||||
|
||||
| Tool | When to Use |
|
||||
|------|-------------|
|
||||
| `mcp__ace-tool__search_context` | Semantic search (primary) |
|
||||
| `mcp__ccw-tools__smart_search` | Symbol/pattern search |
|
||||
| `Grep` | Exact regex matching |
|
||||
| `rg` / `grep` | CLI fallback |
|
||||
| `Glob` | File path discovery |
|
||||
|
||||
#### Phase 3: Solution Planning
|
||||
|
||||
**Multi-Solution Generation**:
|
||||
|
||||
Generate multiple candidate solutions when:
|
||||
- Issue complexity is HIGH
|
||||
- Multiple valid implementation approaches exist
|
||||
- Trade-offs between approaches (performance vs simplicity, etc.)
|
||||
|
||||
| Condition | Solutions |
|
||||
|-----------|-----------|
|
||||
| Low complexity, single approach | 1 solution, auto-bind |
|
||||
| Medium complexity, clear path | 1-2 solutions |
|
||||
| High complexity, multiple approaches | 2-3 solutions, user selection |
|
||||
|
||||
**Solution Evaluation** (for each candidate):
|
||||
```javascript
|
||||
{
|
||||
analysis: {
|
||||
risk: "low|medium|high", // Implementation risk
|
||||
impact: "low|medium|high", // Scope of changes
|
||||
complexity: "low|medium|high" // Technical complexity
|
||||
},
|
||||
score: 0.0-1.0 // Overall quality score (higher = recommended)
|
||||
}
|
||||
```
|
||||
|
||||
**Selection Flow**:
|
||||
1. Generate all candidate solutions
|
||||
2. Evaluate and score each
|
||||
3. Single solution → auto-bind
|
||||
4. Multiple solutions → return `pending_selection` for user choice
|
||||
|
||||
**Task Decomposition** following schema:
|
||||
```javascript
|
||||
function decomposeTasks(issue, exploration) {
|
||||
return groups.map(group => ({
|
||||
id: `TASK-${String(taskId++).padStart(3, '0')}`,
|
||||
id: `T${taskId++}`, // Pattern: ^T[0-9]+$
|
||||
title: group.title,
|
||||
type: inferType(group), // feature | bug | refactor | test | chore | docs
|
||||
scope: inferScope(group), // Module path
|
||||
action: inferAction(group), // Create | Update | Implement | ...
|
||||
description: group.description,
|
||||
file_context: group.files,
|
||||
modification_points: mapModificationPoints(group),
|
||||
implementation: generateSteps(group), // Step-by-step guide
|
||||
test: {
|
||||
unit: generateUnitTests(group),
|
||||
commands: ['npm test']
|
||||
},
|
||||
acceptance: {
|
||||
criteria: generateCriteria(group), // Quantified checklist
|
||||
verification: generateVerification(group)
|
||||
},
|
||||
commit: {
|
||||
type: inferCommitType(group), // feat | fix | refactor | ...
|
||||
scope: inferScope(group),
|
||||
message_template: generateCommitMsg(group)
|
||||
},
|
||||
depends_on: inferDependencies(group, tasks),
|
||||
delivery_criteria: generateDeliveryCriteria(group), // Quantified checklist
|
||||
pause_criteria: identifyBlockers(group),
|
||||
status: 'pending',
|
||||
current_phase: 'analyze',
|
||||
executor: inferExecutor(group),
|
||||
priority: calculatePriority(group)
|
||||
priority: calculatePriority(group) // 1-5 (1=highest)
|
||||
}))
|
||||
}
|
||||
```
|
||||
@@ -139,56 +195,33 @@ ccw issue bind <issue-id> --solution /tmp/sol.json
|
||||
|
||||
---
|
||||
|
||||
## 2. Output Specifications
|
||||
## 2. Output Requirements
|
||||
|
||||
### 2.1 Return Format
|
||||
|
||||
```json
|
||||
{
|
||||
"bound": [{ "issue_id": "...", "solution_id": "...", "task_count": N }],
|
||||
"pending_selection": [{ "issue_id": "...", "solutions": [{ "id": "...", "description": "...", "task_count": N }] }],
|
||||
"conflicts": [{ "file": "...", "issues": [...] }]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Binding Rules
|
||||
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| Single solution | Register AND auto-bind |
|
||||
| Multiple solutions | Register only, return for user selection |
|
||||
|
||||
### 2.3 Task Schema
|
||||
|
||||
**Schema-Driven Output**: Read schema before generating tasks:
|
||||
```bash
|
||||
cat .claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json
|
||||
```
|
||||
|
||||
**Required Fields**:
|
||||
- `id`: Task ID (pattern: `TASK-NNN`)
|
||||
- `title`: Short summary (max 100 chars)
|
||||
- `type`: feature | bug | refactor | test | chore | docs
|
||||
- `description`: Detailed instructions
|
||||
- `depends_on`: Array of prerequisite task IDs
|
||||
- `delivery_criteria`: Checklist items defining completion
|
||||
- `status`: pending | ready | in_progress | completed | failed | paused | skipped
|
||||
- `current_phase`: analyze | implement | test | optimize | commit | done
|
||||
- `executor`: agent | codex | gemini | auto
|
||||
|
||||
**Optional Fields**:
|
||||
- `file_context`: Relevant files/globs
|
||||
- `pause_criteria`: Conditions to halt execution
|
||||
- `priority`: 1-5 (1=highest)
|
||||
- `phase_results`: Results from each execution phase
|
||||
|
||||
### 2.4 Solution File Structure
|
||||
### 2.1 Generate Files (Primary)
|
||||
|
||||
**Solution file per issue**:
|
||||
```
|
||||
.workflow/issues/solutions/{issue-id}.jsonl
|
||||
```
|
||||
|
||||
Each line is a complete solution JSON.
|
||||
Each line is a solution JSON containing tasks. Schema: `cat .claude/workflows/cli-templates/schemas/solution-schema.json`
|
||||
|
||||
### 2.2 Binding
|
||||
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| Single solution | `ccw issue bind <id> --solution <file>` (auto) |
|
||||
| Multiple solutions | Register only, return for selection |
|
||||
|
||||
### 2.3 Return Summary
|
||||
|
||||
```json
|
||||
{
|
||||
"bound": [{ "issue_id": "...", "solution_id": "...", "task_count": N }],
|
||||
"pending_selection": [{ "issue_id": "...", "solutions": [{ "id": "SOL-001", "description": "...", "task_count": N }] }],
|
||||
"conflicts": [{ "file": "...", "issues": [...] }]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -215,12 +248,14 @@ Each line is a complete solution JSON.
|
||||
### 3.3 Guidelines
|
||||
|
||||
**ALWAYS**:
|
||||
1. Read schema first: `cat .claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json`
|
||||
1. Read schema first: `cat .claude/workflows/cli-templates/schemas/solution-schema.json`
|
||||
2. Use ACE semantic search as PRIMARY exploration tool
|
||||
3. Fetch issue details via `ccw issue status <id> --json`
|
||||
4. Quantify delivery_criteria with testable conditions
|
||||
4. Quantify acceptance.criteria with testable conditions
|
||||
5. Validate DAG before output
|
||||
6. Single solution → auto-bind; Multiple → return for selection
|
||||
6. Evaluate each solution with `analysis` and `score`
|
||||
7. Single solution → auto-bind; Multiple → return `pending_selection`
|
||||
8. For HIGH complexity: generate 2-3 candidate solutions
|
||||
|
||||
**NEVER**:
|
||||
1. Execute implementation (return plan only)
|
||||
|
||||
@@ -36,21 +36,21 @@ color: orange
|
||||
```javascript
|
||||
{
|
||||
tasks: [{
|
||||
key: string, // e.g., "GH-123:TASK-001"
|
||||
issue_id: string, // e.g., "GH-123"
|
||||
solution_id: string, // e.g., "SOL-001"
|
||||
task: {
|
||||
id: string, // e.g., "TASK-001"
|
||||
title: string,
|
||||
type: string,
|
||||
file_context: string[],
|
||||
depends_on: string[]
|
||||
}
|
||||
task_id: string, // e.g., "TASK-001"
|
||||
type: string, // feature | bug | refactor | test | chore | docs
|
||||
file_context: string[],
|
||||
depends_on: string[] // composite keys, e.g., ["GH-123:TASK-001"]
|
||||
}],
|
||||
project_root?: string,
|
||||
rebuild?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Agent generates unique `item_id` (pattern: `T-{N}`) for queue output.
|
||||
|
||||
### 1.2 Execution Flow
|
||||
|
||||
```
|
||||
@@ -76,19 +76,17 @@ function buildDependencyGraph(tasks) {
|
||||
const fileModifications = new Map()
|
||||
|
||||
for (const item of tasks) {
|
||||
const key = `${item.issue_id}:${item.task.id}`
|
||||
graph.set(key, { ...item, key, inDegree: 0, outEdges: [] })
|
||||
graph.set(item.key, { ...item, inDegree: 0, outEdges: [] })
|
||||
|
||||
for (const file of item.task.file_context || []) {
|
||||
for (const file of item.file_context || []) {
|
||||
if (!fileModifications.has(file)) fileModifications.set(file, [])
|
||||
fileModifications.get(file).push(key)
|
||||
fileModifications.get(file).push(item.key)
|
||||
}
|
||||
}
|
||||
|
||||
// Add dependency edges
|
||||
for (const [key, node] of graph) {
|
||||
for (const dep of node.task.depends_on || []) {
|
||||
const depKey = `${node.issue_id}:${dep}`
|
||||
for (const depKey of node.depends_on || []) {
|
||||
if (graph.has(depKey)) {
|
||||
graph.get(depKey).outEdges.push(key)
|
||||
node.inDegree++
|
||||
@@ -128,6 +126,16 @@ function detectConflicts(fileModifications, graph) {
|
||||
|
||||
### 2.4 Semantic Priority
|
||||
|
||||
**Base Priority Mapping** (task.priority 1-5 → base score):
|
||||
| task.priority | Base Score | Meaning |
|
||||
|---------------|------------|---------|
|
||||
| 1 | 0.8 | Highest |
|
||||
| 2 | 0.65 | High |
|
||||
| 3 | 0.5 | Medium |
|
||||
| 4 | 0.35 | Low |
|
||||
| 5 | 0.2 | Lowest |
|
||||
|
||||
**Action-based Boost** (applied to base score):
|
||||
| Factor | Boost |
|
||||
|--------|-------|
|
||||
| Create action | +0.2 |
|
||||
@@ -140,6 +148,8 @@ function detectConflicts(fileModifications, graph) {
|
||||
| Test action | -0.1 |
|
||||
| Delete action | -0.15 |
|
||||
|
||||
**Formula**: `semantic_priority = clamp(baseScore + sum(boosts), 0.0, 1.0)`
|
||||
|
||||
### 2.5 Group Assignment
|
||||
|
||||
- **Parallel (P*)**: Tasks with no dependencies or conflicts between them
|
||||
@@ -147,48 +157,29 @@ function detectConflicts(fileModifications, graph) {
|
||||
|
||||
---
|
||||
|
||||
## 3. Output Specifications
|
||||
## 3. Output Requirements
|
||||
|
||||
### 3.1 Queue Schema
|
||||
### 3.1 Generate Files (Primary)
|
||||
|
||||
Read schema before output:
|
||||
```bash
|
||||
cat .claude/workflows/cli-templates/schemas/queue-schema.json
|
||||
**Queue files**:
|
||||
```
|
||||
.workflow/issues/queues/{queue-id}.json # Full queue with tasks, conflicts, groups
|
||||
.workflow/issues/queues/index.json # Update with new queue entry
|
||||
```
|
||||
|
||||
### 3.2 Output Format
|
||||
Queue ID format: `QUE-YYYYMMDD-HHMMSS` (UTC timestamp)
|
||||
|
||||
Schema: `cat .claude/workflows/cli-templates/schemas/queue-schema.json`
|
||||
|
||||
### 3.2 Return Summary
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": [{
|
||||
"item_id": "T-1",
|
||||
"issue_id": "GH-123",
|
||||
"solution_id": "SOL-001",
|
||||
"task_id": "TASK-001",
|
||||
"status": "pending",
|
||||
"execution_order": 1,
|
||||
"execution_group": "P1",
|
||||
"depends_on": [],
|
||||
"semantic_priority": 0.7
|
||||
}],
|
||||
"conflicts": [{
|
||||
"file": "src/auth.ts",
|
||||
"tasks": ["GH-123:TASK-001", "GH-124:TASK-002"],
|
||||
"resolution": "sequential",
|
||||
"resolution_order": ["GH-123:TASK-001", "GH-124:TASK-002"],
|
||||
"rationale": "TASK-001 creates file before TASK-002 updates",
|
||||
"resolved": true
|
||||
}],
|
||||
"execution_groups": [
|
||||
{ "id": "P1", "type": "parallel", "task_count": 3, "tasks": ["T-1", "T-2", "T-3"] },
|
||||
{ "id": "S2", "type": "sequential", "task_count": 2, "tasks": ["T-4", "T-5"] }
|
||||
],
|
||||
"_metadata": {
|
||||
"total_tasks": 5,
|
||||
"total_conflicts": 1,
|
||||
"resolved_conflicts": 1,
|
||||
"timestamp": "2025-12-27T10:00:00Z"
|
||||
}
|
||||
"queue_id": "QUE-20251227-143000",
|
||||
"total_tasks": N,
|
||||
"execution_groups": [{ "id": "P1", "type": "parallel", "count": N }],
|
||||
"conflicts_resolved": N,
|
||||
"issues_queued": ["GH-123", "GH-124"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -231,5 +222,6 @@ cat .claude/workflows/cli-templates/schemas/queue-schema.json
|
||||
5. Merge conflicting tasks in parallel group
|
||||
|
||||
**OUTPUT**:
|
||||
1. Write queue via `ccw issue queue` CLI
|
||||
2. Return JSON with `tasks`, `conflicts`, `execution_groups`, `_metadata`
|
||||
1. Write `.workflow/issues/queues/{queue-id}.json`
|
||||
2. Update `.workflow/issues/queues/index.json`
|
||||
3. Return summary with `queue_id`, `total_tasks`, `execution_groups`, `conflicts_resolved`, `issues_queued`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: plan
|
||||
description: Batch plan issue resolution using issue-plan-agent (explore + plan closed-loop)
|
||||
argument-hint: "<issue-id>[,<issue-id>,...] [--batch-size 3] --all-pending"
|
||||
argument-hint: "--all-pending <issue-id>[,<issue-id>,...] [--batch-size 3] "
|
||||
allowed-tools: TodoWrite(*), Task(*), SlashCommand(*), AskUserQuestion(*), Bash(*), Read(*), Write(*)
|
||||
---
|
||||
|
||||
@@ -29,8 +29,8 @@ Unified planning command using **issue-plan-agent** that combines exploration an
|
||||
- [ ] Solution file generated for each issue
|
||||
- [ ] Single solution → auto-bound via `ccw issue bind`
|
||||
- [ ] Multiple solutions → returned for user selection
|
||||
- [ ] Tasks conform to schema: `cat .claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json`
|
||||
- [ ] Each task has quantified `delivery_criteria`
|
||||
- [ ] Tasks conform to schema: `cat .claude/workflows/cli-templates/schemas/solution-schema.json`
|
||||
- [ ] Each task has quantified `acceptance.criteria`
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
@@ -70,9 +70,9 @@ Unified planning command using **issue-plan-agent** that combines exploration an
|
||||
```
|
||||
Phase 1: Issue Loading
|
||||
├─ Parse input (single, comma-separated, or --all-pending)
|
||||
├─ Load issues from .workflow/issues/issues.jsonl
|
||||
├─ Fetch issue metadata (ID, title, tags)
|
||||
├─ Validate issues exist (create if needed)
|
||||
└─ Group into batches (max 3 per batch)
|
||||
└─ Group by similarity (shared tags or title keywords, max 3 per batch)
|
||||
|
||||
Phase 2: Unified Explore + Plan (issue-plan-agent)
|
||||
├─ Launch issue-plan-agent per batch
|
||||
@@ -97,41 +97,71 @@ Phase 4: Summary
|
||||
|
||||
## Implementation
|
||||
|
||||
### Phase 1: Issue Loading (IDs Only)
|
||||
### Phase 1: Issue Loading (ID + Title + Tags)
|
||||
|
||||
```javascript
|
||||
const batchSize = flags.batchSize || 3;
|
||||
let issueIds = [];
|
||||
let issues = []; // {id, title, tags}
|
||||
|
||||
if (flags.allPending) {
|
||||
// Get pending issue IDs directly via CLI
|
||||
const ids = Bash(`ccw issue list --status pending,registered --ids`).trim();
|
||||
issueIds = ids ? ids.split('\n').filter(Boolean) : [];
|
||||
// Get pending issues with metadata via CLI (JSON output)
|
||||
const result = Bash(`ccw issue list --status pending,registered --json`).trim();
|
||||
const parsed = result ? JSON.parse(result) : [];
|
||||
issues = parsed.map(i => ({ id: i.id, title: i.title || '', tags: i.tags || [] }));
|
||||
|
||||
if (issueIds.length === 0) {
|
||||
if (issues.length === 0) {
|
||||
console.log('No pending issues found.');
|
||||
return;
|
||||
}
|
||||
console.log(`Found ${issueIds.length} pending issues`);
|
||||
console.log(`Found ${issues.length} pending issues`);
|
||||
} else {
|
||||
// Parse comma-separated issue IDs
|
||||
issueIds = userInput.includes(',')
|
||||
// Parse comma-separated issue IDs, fetch metadata
|
||||
const ids = userInput.includes(',')
|
||||
? userInput.split(',').map(s => s.trim())
|
||||
: [userInput.trim()];
|
||||
|
||||
// Create if not exists
|
||||
for (const id of issueIds) {
|
||||
for (const id of ids) {
|
||||
Bash(`ccw issue init ${id} --title "Issue ${id}" 2>/dev/null || true`);
|
||||
const info = Bash(`ccw issue status ${id} --json`).trim();
|
||||
const parsed = info ? JSON.parse(info) : {};
|
||||
issues.push({ id, title: parsed.title || '', tags: parsed.tags || [] });
|
||||
}
|
||||
}
|
||||
|
||||
// Group into batches
|
||||
const batches = [];
|
||||
for (let i = 0; i < issueIds.length; i += batchSize) {
|
||||
batches.push(issueIds.slice(i, i + batchSize));
|
||||
// Intelligent grouping by similarity (tags → title keywords)
|
||||
function groupBySimilarity(issues, maxSize) {
|
||||
const batches = [];
|
||||
const used = new Set();
|
||||
|
||||
for (const issue of issues) {
|
||||
if (used.has(issue.id)) continue;
|
||||
|
||||
const batch = [issue];
|
||||
used.add(issue.id);
|
||||
const issueTags = new Set(issue.tags);
|
||||
const issueWords = new Set(issue.title.toLowerCase().split(/\s+/));
|
||||
|
||||
// Find similar issues
|
||||
for (const other of issues) {
|
||||
if (used.has(other.id) || batch.length >= maxSize) continue;
|
||||
|
||||
// Similarity: shared tags or shared title keywords
|
||||
const sharedTags = other.tags.filter(t => issueTags.has(t)).length;
|
||||
const otherWords = other.title.toLowerCase().split(/\s+/);
|
||||
const sharedWords = otherWords.filter(w => issueWords.has(w) && w.length > 3).length;
|
||||
|
||||
if (sharedTags > 0 || sharedWords >= 2) {
|
||||
batch.push(other);
|
||||
used.add(other.id);
|
||||
}
|
||||
}
|
||||
batches.push(batch);
|
||||
}
|
||||
return batches;
|
||||
}
|
||||
|
||||
console.log(`Processing ${issueIds.length} issues in ${batches.length} batch(es)`);
|
||||
const batches = groupBySimilarity(issues, batchSize);
|
||||
console.log(`Processing ${issues.length} issues in ${batches.length} batch(es) (grouped by similarity)`);
|
||||
|
||||
TodoWrite({
|
||||
todos: batches.map((_, i) => ({
|
||||
@@ -151,11 +181,16 @@ const pendingSelections = []; // Collect multi-solution issues for user selecti
|
||||
for (const [batchIndex, batch] of batches.entries()) {
|
||||
updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress');
|
||||
|
||||
// Build issue list with metadata for agent context
|
||||
const issueList = batch.map(i => `- ${i.id}: ${i.title}${i.tags.length ? ` [${i.tags.join(', ')}]` : ''}`).join('\n');
|
||||
|
||||
// Build minimal prompt - agent handles exploration, planning, and binding
|
||||
const issuePrompt = `
|
||||
## Plan Issues
|
||||
|
||||
**Issue IDs**: ${batch.join(', ')}
|
||||
**Issues** (grouped by similarity):
|
||||
${issueList}
|
||||
|
||||
**Project Root**: ${process.cwd()}
|
||||
|
||||
### Steps
|
||||
@@ -164,7 +199,7 @@ for (const [batchIndex, batch] of batches.entries()) {
|
||||
3. Register & bind: \`ccw issue bind <id> --solution <file>\`
|
||||
|
||||
### Generate Files
|
||||
\`.workflow/issues/solutions/{issue-id}.jsonl\` - Solution with tasks (schema: cat .claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json)
|
||||
\`.workflow/issues/solutions/{issue-id}.jsonl\` - Solution with tasks (schema: cat .claude/workflows/cli-templates/schemas/solution-schema.json)
|
||||
|
||||
### Binding Rules
|
||||
- **Single solution**: Auto-bind via \`ccw issue bind <id> --solution <file>\`
|
||||
@@ -181,10 +216,11 @@ for (const [batchIndex, batch] of batches.entries()) {
|
||||
`;
|
||||
|
||||
// Launch issue-plan-agent - agent writes solutions directly
|
||||
const batchIds = batch.map(i => i.id);
|
||||
const result = Task(
|
||||
subagent_type="issue-plan-agent",
|
||||
run_in_background=false,
|
||||
description=`Explore & plan ${batch.length} issues`,
|
||||
description=`Explore & plan ${batch.length} issues: ${batchIds.join(', ')}`,
|
||||
prompt=issuePrompt
|
||||
);
|
||||
|
||||
@@ -244,7 +280,7 @@ const plannedIds = Bash(`ccw issue list --status planned --ids`).trim();
|
||||
const plannedCount = plannedIds ? plannedIds.split('\n').length : 0;
|
||||
|
||||
console.log(`
|
||||
## Done: ${issueIds.length} issues → ${plannedCount} planned
|
||||
## Done: ${issues.length} issues → ${plannedCount} planned
|
||||
|
||||
Next: \`/issue:queue\` → \`/issue:execute\`
|
||||
`);
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Issue Task JSONL Schema",
|
||||
"description": "Schema for individual task entries in tasks.jsonl file",
|
||||
"type": "object",
|
||||
"required": ["id", "title", "type", "description", "depends_on", "delivery_criteria", "status", "current_phase", "executor"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique task identifier (e.g., TASK-001)",
|
||||
"pattern": "^TASK-[0-9]+$"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Short summary of the task",
|
||||
"maxLength": 100
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["feature", "bug", "refactor", "test", "chore", "docs"],
|
||||
"description": "Task category"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Detailed instructions for the task"
|
||||
},
|
||||
"file_context": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "List of relevant files/globs",
|
||||
"default": []
|
||||
},
|
||||
"depends_on": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Array of Task IDs that must complete first",
|
||||
"default": []
|
||||
},
|
||||
"delivery_criteria": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Checklist items that define task completion",
|
||||
"minItems": 1
|
||||
},
|
||||
"pause_criteria": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Conditions that should halt execution (e.g., 'API spec unclear')",
|
||||
"default": []
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "ready", "in_progress", "completed", "failed", "paused", "skipped"],
|
||||
"description": "Current task status",
|
||||
"default": "pending"
|
||||
},
|
||||
"current_phase": {
|
||||
"type": "string",
|
||||
"enum": ["analyze", "implement", "test", "optimize", "commit", "done"],
|
||||
"description": "Current execution phase within the task lifecycle",
|
||||
"default": "analyze"
|
||||
},
|
||||
"executor": {
|
||||
"type": "string",
|
||||
"enum": ["agent", "codex", "gemini", "auto"],
|
||||
"description": "Preferred executor for this task",
|
||||
"default": "auto"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 5,
|
||||
"description": "Task priority (1=highest, 5=lowest)",
|
||||
"default": 3
|
||||
},
|
||||
"phase_results": {
|
||||
"type": "object",
|
||||
"description": "Results from each execution phase",
|
||||
"properties": {
|
||||
"analyze": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": { "type": "string" },
|
||||
"findings": { "type": "array", "items": { "type": "string" } },
|
||||
"timestamp": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"implement": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": { "type": "string" },
|
||||
"files_modified": { "type": "array", "items": { "type": "string" } },
|
||||
"timestamp": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": { "type": "string" },
|
||||
"test_results": { "type": "string" },
|
||||
"retry_count": { "type": "integer" },
|
||||
"timestamp": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"optimize": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": { "type": "string" },
|
||||
"improvements": { "type": "array", "items": { "type": "string" } },
|
||||
"timestamp": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"commit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": { "type": "string" },
|
||||
"commit_hash": { "type": "string" },
|
||||
"message": { "type": "string" },
|
||||
"timestamp": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Task creation timestamp"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Last update timestamp"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -9,11 +9,11 @@
|
||||
"description": "Ordered list of tasks to execute",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["queue_id", "issue_id", "solution_id", "task_id", "status"],
|
||||
"required": ["item_id", "issue_id", "solution_id", "task_id", "status"],
|
||||
"properties": {
|
||||
"queue_id": {
|
||||
"item_id": {
|
||||
"type": "string",
|
||||
"pattern": "^Q-[0-9]+$",
|
||||
"pattern": "^T-[0-9]+$",
|
||||
"description": "Unique queue item identifier"
|
||||
},
|
||||
"issue_id": {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"description": "Task breakdown for this solution",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "title", "scope", "action", "acceptance"],
|
||||
"required": ["id", "title", "scope", "action", "implementation", "acceptance"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
@@ -61,10 +61,40 @@
|
||||
"items": { "type": "string" },
|
||||
"description": "Step-by-step implementation guide"
|
||||
},
|
||||
"acceptance": {
|
||||
"test": {
|
||||
"type": "object",
|
||||
"description": "Test requirements",
|
||||
"properties": {
|
||||
"unit": { "type": "array", "items": { "type": "string" } },
|
||||
"integration": { "type": "array", "items": { "type": "string" } },
|
||||
"commands": { "type": "array", "items": { "type": "string" } },
|
||||
"coverage_target": { "type": "number" }
|
||||
}
|
||||
},
|
||||
"regression": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Quantified completion criteria"
|
||||
"description": "Regression check points"
|
||||
},
|
||||
"acceptance": {
|
||||
"type": "object",
|
||||
"description": "Acceptance criteria & verification",
|
||||
"required": ["criteria", "verification"],
|
||||
"properties": {
|
||||
"criteria": { "type": "array", "items": { "type": "string" } },
|
||||
"verification": { "type": "array", "items": { "type": "string" } },
|
||||
"manual_checks": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
"commit": {
|
||||
"type": "object",
|
||||
"description": "Commit specification",
|
||||
"properties": {
|
||||
"type": { "type": "string", "enum": ["feat", "fix", "refactor", "test", "docs", "chore"] },
|
||||
"scope": { "type": "string" },
|
||||
"message_template": { "type": "string" },
|
||||
"breaking": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"depends_on": {
|
||||
"type": "array",
|
||||
@@ -80,6 +110,28 @@
|
||||
"type": "string",
|
||||
"enum": ["codex", "gemini", "agent", "auto"],
|
||||
"default": "auto"
|
||||
},
|
||||
"lifecycle_status": {
|
||||
"type": "object",
|
||||
"description": "Lifecycle phase tracking",
|
||||
"properties": {
|
||||
"implemented": { "type": "boolean" },
|
||||
"tested": { "type": "boolean" },
|
||||
"regression_passed": { "type": "boolean" },
|
||||
"accepted": { "type": "boolean" },
|
||||
"committed": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "ready", "executing", "completed", "failed", "blocked"],
|
||||
"default": "pending"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 5,
|
||||
"default": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,6 +149,21 @@
|
||||
"integration_points": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"analysis": {
|
||||
"type": "object",
|
||||
"description": "Solution risk assessment",
|
||||
"properties": {
|
||||
"risk": { "type": "string", "enum": ["low", "medium", "high"] },
|
||||
"impact": { "type": "string", "enum": ["low", "medium", "high"] },
|
||||
"complexity": { "type": "string", "enum": ["low", "medium", "high"] }
|
||||
}
|
||||
},
|
||||
"score": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Solution quality score (0.0-1.0)"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["draft", "candidate", "bound", "queued", "executing", "completed", "failed"],
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Solutions JSONL Schema",
|
||||
"description": "Schema for each line in solutions/{issue-id}.jsonl",
|
||||
"type": "object",
|
||||
"required": ["id", "tasks", "created_at"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique solution identifier",
|
||||
"pattern": "^SOL-[0-9]+$"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Solution approach description"
|
||||
},
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"description": "Task breakdown for this solution",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "title", "scope", "action", "acceptance"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^T[0-9]+$"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Action verb + target"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"description": "Module path or feature area"
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["Create", "Update", "Implement", "Refactor", "Add", "Delete", "Configure", "Test", "Fix"]
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "1-2 sentences describing what to implement"
|
||||
},
|
||||
"modification_points": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": { "type": "string" },
|
||||
"target": { "type": "string" },
|
||||
"change": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"implementation": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Step-by-step implementation guide"
|
||||
},
|
||||
"acceptance": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Quantified completion criteria"
|
||||
},
|
||||
"depends_on": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"default": [],
|
||||
"description": "Task IDs this task depends on"
|
||||
},
|
||||
"estimated_minutes": {
|
||||
"type": "integer",
|
||||
"description": "Estimated time to complete"
|
||||
},
|
||||
"executor": {
|
||||
"type": "string",
|
||||
"enum": ["codex", "gemini", "agent", "auto"],
|
||||
"default": "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exploration_context": {
|
||||
"type": "object",
|
||||
"description": "ACE exploration results",
|
||||
"properties": {
|
||||
"project_structure": { "type": "string" },
|
||||
"relevant_files": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"patterns": { "type": "string" },
|
||||
"integration_points": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"analysis": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"risk": { "type": "string", "enum": ["low", "medium", "high"] },
|
||||
"impact": { "type": "string", "enum": ["low", "medium", "high"] },
|
||||
"complexity": { "type": "string", "enum": ["low", "medium", "high"] }
|
||||
}
|
||||
},
|
||||
"score": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Solution quality score (0.0-1.0)"
|
||||
},
|
||||
"is_bound": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Whether this solution is bound to the issue"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"bound_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When this solution was bound to the issue"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -5,6 +5,29 @@ All notable changes to Claude Code Workflow (CCW) will be documented in this fil
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [6.3.9] - 2025-12-27
|
||||
|
||||
### 🔧 Issue System Consistency | Issue系统一致性修复
|
||||
|
||||
#### Schema Unification | Schema统一
|
||||
- **Upgraded**: `solution-schema.json` to Rich Plan model with full lifecycle fields
|
||||
- **Added**: `test`, `regression`, `commit`, `lifecycle_status` objects to task schema
|
||||
- **Changed**: `acceptance` from string[] to object `{criteria[], verification[]}`
|
||||
- **Added**: `analysis` and `score` fields for multi-solution evaluation
|
||||
- **Removed**: Redundant `issue-task-jsonl-schema.json` and `solutions-jsonl-schema.json`
|
||||
- **Fixed**: `queue-schema.json` field naming (`queue_id` → `item_id`)
|
||||
|
||||
#### Agent Updates | Agent更新
|
||||
- **Added**: Multi-solution generation support based on complexity
|
||||
- **Added**: Search tool fallback chain (ACE → smart_search → Grep → rg → Glob)
|
||||
- **Added**: `lifecycle_requirements` propagation from issue to tasks
|
||||
- **Added**: Priority mapping formula (1-5 → 0.0-1.0 semantic priority)
|
||||
- **Fixed**: Task decomposition to match Rich Plan schema
|
||||
|
||||
#### Type Safety | 类型安全
|
||||
- **Added**: `QueueConflict` and `ExecutionGroup` interfaces to `issue.ts`
|
||||
- **Fixed**: `conflicts` array typing (from `any[]` to `QueueConflict[]`)
|
||||
|
||||
## [6.2.0] - 2025-12-21
|
||||
|
||||
### 🎯 Native CodexLens & Dashboard Revolution | 原生CodexLens与Dashboard革新
|
||||
|
||||
@@ -129,14 +129,31 @@ interface QueueItem {
|
||||
failure_reason?: string;
|
||||
}
|
||||
|
||||
interface QueueConflict {
|
||||
type: 'file_conflict' | 'dependency_conflict' | 'resource_conflict';
|
||||
tasks: string[]; // Item IDs involved in conflict
|
||||
file?: string; // Conflicting file path
|
||||
resolution: 'sequential' | 'merge' | 'manual';
|
||||
resolution_order?: string[];
|
||||
rationale?: string;
|
||||
resolved: boolean;
|
||||
}
|
||||
|
||||
interface ExecutionGroup {
|
||||
id: string; // Group ID: P1, S1, etc.
|
||||
type: 'parallel' | 'sequential';
|
||||
task_count: number;
|
||||
tasks: string[]; // Item IDs in this group
|
||||
}
|
||||
|
||||
interface Queue {
|
||||
id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS (derived from filename)
|
||||
name?: string; // Optional queue name
|
||||
status: 'active' | 'completed' | 'archived' | 'failed';
|
||||
issue_ids: string[]; // Issues in this queue
|
||||
tasks: QueueItem[]; // Task items (formerly 'queue')
|
||||
conflicts: any[];
|
||||
execution_groups?: any[];
|
||||
conflicts: QueueConflict[];
|
||||
execution_groups?: ExecutionGroup[];
|
||||
_metadata: {
|
||||
version: string;
|
||||
total_tasks: number;
|
||||
|
||||
@@ -236,6 +236,82 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/queue/history - Get queue history (all queues from index)
|
||||
if (pathname === '/api/queue/history' && req.method === 'GET') {
|
||||
const queuesDir = join(issuesDir, 'queues');
|
||||
const indexPath = join(queuesDir, 'index.json');
|
||||
|
||||
if (!existsSync(indexPath)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ queues: [], active_queue_id: null }));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(index));
|
||||
} catch {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ queues: [], active_queue_id: null }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/queue/:id - Get specific queue by ID
|
||||
const queueDetailMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
|
||||
if (queueDetailMatch && req.method === 'GET' && queueDetailMatch[1] !== 'history' && queueDetailMatch[1] !== 'reorder') {
|
||||
const queueId = queueDetailMatch[1];
|
||||
const queuesDir = join(issuesDir, 'queues');
|
||||
const queueFilePath = join(queuesDir, `${queueId}.json`);
|
||||
|
||||
if (!existsSync(queueFilePath)) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: `Queue ${queueId} not found` }));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const queue = JSON.parse(readFileSync(queueFilePath, 'utf8'));
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(groupQueueByExecutionGroup(queue)));
|
||||
} catch {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Failed to read queue' }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/queue/switch - Switch active queue
|
||||
if (pathname === '/api/queue/switch' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const { queueId } = body;
|
||||
if (!queueId) return { error: 'queueId required' };
|
||||
|
||||
const queuesDir = join(issuesDir, 'queues');
|
||||
const indexPath = join(queuesDir, 'index.json');
|
||||
const queueFilePath = join(queuesDir, `${queueId}.json`);
|
||||
|
||||
if (!existsSync(queueFilePath)) {
|
||||
return { error: `Queue ${queueId} not found` };
|
||||
}
|
||||
|
||||
try {
|
||||
const index = existsSync(indexPath)
|
||||
? JSON.parse(readFileSync(indexPath, 'utf8'))
|
||||
: { active_queue_id: null, queues: [] };
|
||||
|
||||
index.active_queue_id = queueId;
|
||||
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
||||
|
||||
return { success: true, active_queue_id: queueId };
|
||||
} catch (err) {
|
||||
return { error: 'Failed to switch queue' };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/queue/reorder - Reorder queue items
|
||||
if (pathname === '/api/queue/reorder' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
|
||||
@@ -285,9 +285,23 @@
|
||||
|
||||
.queue-empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.queue-empty-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0.5rem 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.queue-empty-container .queue-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.queue-empty-title {
|
||||
@@ -2542,3 +2556,298 @@
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
QUEUE HISTORY MODAL
|
||||
========================================== */
|
||||
|
||||
.queue-history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.queue-history-item {
|
||||
padding: 1rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.queue-history-item:hover {
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.queue-history-item.active {
|
||||
border-color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
}
|
||||
|
||||
.queue-history-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.queue-history-id {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.queue-active-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.queue-history-status {
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.queue-history-status.active {
|
||||
background: hsl(142 76% 36% / 0.2);
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.queue-history-status.completed {
|
||||
background: hsl(142 76% 36% / 0.2);
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.queue-history-status.archived {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.queue-history-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.queue-history-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Queue Detail View */
|
||||
.queue-detail-view {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.queue-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.queue-detail-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.queue-detail-stats .stat-item {
|
||||
text-align: center;
|
||||
padding: 0.75rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.queue-detail-stats .stat-value {
|
||||
display: block;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.queue-detail-stats .stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.queue-detail-stats .stat-item.completed .stat-value {
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.queue-detail-stats .stat-item.pending .stat-value {
|
||||
color: hsl(48 96% 53%);
|
||||
}
|
||||
|
||||
.queue-detail-stats .stat-item.failed .stat-value {
|
||||
color: hsl(0 84% 60%);
|
||||
}
|
||||
|
||||
.queue-detail-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.queue-group-section {
|
||||
background: hsl(var(--muted) / 0.2);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.queue-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.queue-group-items {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.queue-detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.queue-detail-item:hover {
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
}
|
||||
|
||||
.queue-detail-item.completed {
|
||||
border-left-color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.queue-detail-item.pending {
|
||||
border-left-color: hsl(48 96% 53%);
|
||||
}
|
||||
|
||||
.queue-detail-item.executing {
|
||||
border-left-color: hsl(217 91% 60%);
|
||||
}
|
||||
|
||||
.queue-detail-item.failed {
|
||||
border-left-color: hsl(0 84% 60%);
|
||||
}
|
||||
|
||||
.queue-detail-item .item-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.queue-detail-item .item-id {
|
||||
min-width: 50px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.queue-detail-item .item-title {
|
||||
flex: 1;
|
||||
color: hsl(var(--foreground));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.queue-detail-item .item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding-left: calc(50px + 0.75rem);
|
||||
}
|
||||
|
||||
.queue-detail-item .item-issue {
|
||||
color: hsl(var(--primary));
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.queue-detail-item .item-status {
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.queue-detail-item .item-status.completed {
|
||||
background: hsl(142 76% 36% / 0.2);
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.queue-detail-item .item-status.pending {
|
||||
background: hsl(48 96% 53% / 0.2);
|
||||
color: hsl(48 96% 53%);
|
||||
}
|
||||
|
||||
.queue-detail-item .item-status.executing {
|
||||
background: hsl(217 91% 60% / 0.2);
|
||||
color: hsl(217 91% 60%);
|
||||
}
|
||||
|
||||
.queue-detail-item .item-status.failed {
|
||||
background: hsl(0 84% 60% / 0.2);
|
||||
color: hsl(0 84% 60%);
|
||||
}
|
||||
|
||||
/* Small Buttons */
|
||||
.btn-sm {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-sm.btn-primary {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.btn-sm.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-sm.btn-secondary {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.btn-sm.btn-secondary:hover {
|
||||
background: hsl(var(--muted) / 0.8);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.queue-detail-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.queue-history-meta {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +367,12 @@ function renderQueueSection() {
|
||||
if (queueItems.length === 0) {
|
||||
return `
|
||||
<div class="queue-empty-container">
|
||||
<div class="queue-empty-toolbar">
|
||||
<button class="btn-secondary" onclick="showQueueHistoryModal()" title="${t('issues.queueHistory') || 'Queue History'}">
|
||||
<i data-lucide="history" class="w-4 h-4"></i>
|
||||
<span>${t('issues.history') || 'History'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="queue-empty">
|
||||
<i data-lucide="git-branch" class="w-16 h-16"></i>
|
||||
<p class="queue-empty-title">${t('issues.queueEmpty') || 'Queue is empty'}</p>
|
||||
@@ -423,6 +429,10 @@ function renderQueueSection() {
|
||||
<button class="btn-secondary" onclick="refreshQueue()" title="${t('issues.refreshQueue') || 'Refresh'}">
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button class="btn-secondary" onclick="showQueueHistoryModal()" title="${t('issues.queueHistory') || 'Queue History'}">
|
||||
<i data-lucide="history" class="w-4 h-4"></i>
|
||||
<span>${t('issues.history') || 'History'}</span>
|
||||
</button>
|
||||
<button class="btn-secondary" onclick="createExecutionQueue()" title="${t('issues.regenerateQueue') || 'Regenerate Queue'}">
|
||||
<i data-lucide="rotate-cw" class="w-4 h-4"></i>
|
||||
<span>${t('issues.regenerate') || 'Regenerate'}</span>
|
||||
@@ -1529,6 +1539,248 @@ function hideQueueCommandModal() {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Queue History Modal ==========
|
||||
async function showQueueHistoryModal() {
|
||||
// Create modal if not exists
|
||||
let modal = document.getElementById('queueHistoryModal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'queueHistoryModal';
|
||||
modal.className = 'issue-modal';
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
modal.innerHTML = `
|
||||
<div class="issue-modal-backdrop" onclick="hideQueueHistoryModal()"></div>
|
||||
<div class="issue-modal-content" style="max-width: 700px; max-height: 80vh;">
|
||||
<div class="issue-modal-header">
|
||||
<h3><i data-lucide="history" class="w-5 h-5 inline mr-2"></i>Queue History</h3>
|
||||
<button class="btn-icon" onclick="hideQueueHistoryModal()">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="issue-modal-body" style="overflow-y: auto; max-height: calc(80vh - 120px);">
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<i data-lucide="loader-2" class="w-6 h-6 animate-spin"></i>
|
||||
<span class="ml-2">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
modal.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
|
||||
// Fetch queue history
|
||||
try {
|
||||
const response = await fetch(`/api/queue/history?path=${encodeURIComponent(projectPath)}`);
|
||||
const data = await response.json();
|
||||
|
||||
const queues = data.queues || [];
|
||||
const activeQueueId = data.active_queue_id;
|
||||
|
||||
// Render queue list
|
||||
const queueListHtml = queues.length === 0
|
||||
? `<div class="text-center py-8 text-muted-foreground">
|
||||
<i data-lucide="inbox" class="w-12 h-12 mx-auto mb-2 opacity-50"></i>
|
||||
<p>No queue history found</p>
|
||||
</div>`
|
||||
: `<div class="queue-history-list">
|
||||
${queues.map(q => `
|
||||
<div class="queue-history-item ${q.id === activeQueueId ? 'active' : ''}" onclick="viewQueueDetail('${q.id}')">
|
||||
<div class="queue-history-header">
|
||||
<span class="queue-history-id font-mono">${q.id}</span>
|
||||
${q.id === activeQueueId ? '<span class="queue-active-badge">Active</span>' : ''}
|
||||
<span class="queue-history-status ${q.status || ''}">${q.status || 'unknown'}</span>
|
||||
</div>
|
||||
<div class="queue-history-meta">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
<i data-lucide="layers" class="w-3 h-3 inline"></i>
|
||||
${q.issue_ids?.length || 0} issues
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
<i data-lucide="check-circle" class="w-3 h-3 inline"></i>
|
||||
${q.completed_tasks || 0}/${q.total_tasks || 0} tasks
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
<i data-lucide="calendar" class="w-3 h-3 inline"></i>
|
||||
${q.created_at ? new Date(q.created_at).toLocaleDateString() : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="queue-history-actions">
|
||||
${q.id !== activeQueueId ? `
|
||||
<button class="btn-sm btn-primary" onclick="event.stopPropagation(); switchToQueue('${q.id}')">
|
||||
<i data-lucide="arrow-right-circle" class="w-3 h-3"></i>
|
||||
Switch
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn-sm btn-secondary" onclick="event.stopPropagation(); viewQueueDetail('${q.id}')">
|
||||
<i data-lucide="eye" class="w-3 h-3"></i>
|
||||
View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>`;
|
||||
|
||||
modal.querySelector('.issue-modal-body').innerHTML = queueListHtml;
|
||||
lucide.createIcons();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to load queue history:', err);
|
||||
modal.querySelector('.issue-modal-body').innerHTML = `
|
||||
<div class="text-center py-8 text-red-500">
|
||||
<i data-lucide="alert-circle" class="w-8 h-8 mx-auto mb-2"></i>
|
||||
<p>Failed to load queue history</p>
|
||||
</div>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function hideQueueHistoryModal() {
|
||||
const modal = document.getElementById('queueHistoryModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function switchToQueue(queueId) {
|
||||
try {
|
||||
const response = await fetch(`/api/queue/switch?path=${encodeURIComponent(projectPath)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ queueId })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showNotification(t('issues.queueSwitched') || 'Switched to queue: ' + queueId, 'success');
|
||||
hideQueueHistoryModal();
|
||||
await loadQueueData();
|
||||
renderIssueView();
|
||||
} else {
|
||||
showNotification(result.error || 'Failed to switch queue', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to switch queue:', err);
|
||||
showNotification('Failed to switch queue', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function viewQueueDetail(queueId) {
|
||||
const modal = document.getElementById('queueHistoryModal');
|
||||
if (!modal) return;
|
||||
|
||||
// Show loading
|
||||
modal.querySelector('.issue-modal-body').innerHTML = `
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<i data-lucide="loader-2" class="w-6 h-6 animate-spin"></i>
|
||||
<span class="ml-2">${t('common.loading') || 'Loading...'}</span>
|
||||
</div>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/queue/${queueId}?path=${encodeURIComponent(projectPath)}`);
|
||||
const queue = await response.json();
|
||||
|
||||
if (queue.error) {
|
||||
throw new Error(queue.error);
|
||||
}
|
||||
|
||||
const tasks = queue.queue || [];
|
||||
const metadata = queue._metadata || {};
|
||||
|
||||
// Group by execution_group
|
||||
const grouped = {};
|
||||
tasks.forEach(task => {
|
||||
const group = task.execution_group || 'ungrouped';
|
||||
if (!grouped[group]) grouped[group] = [];
|
||||
grouped[group].push(task);
|
||||
});
|
||||
|
||||
const detailHtml = `
|
||||
<div class="queue-detail-view">
|
||||
<div class="queue-detail-header mb-4">
|
||||
<button class="btn-sm btn-secondary" onclick="showQueueHistoryModal()">
|
||||
<i data-lucide="arrow-left" class="w-3 h-3"></i>
|
||||
Back
|
||||
</button>
|
||||
<div class="ml-4">
|
||||
<h4 class="text-lg font-semibold">${queue.name || queue.id || queueId}</h4>
|
||||
${queue.name ? `<span class="text-xs text-muted-foreground font-mono">${queue.id}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="queue-detail-stats mb-4">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">${tasks.length}</span>
|
||||
<span class="stat-label">Total</span>
|
||||
</div>
|
||||
<div class="stat-item completed">
|
||||
<span class="stat-value">${tasks.filter(t => t.status === 'completed').length}</span>
|
||||
<span class="stat-label">Completed</span>
|
||||
</div>
|
||||
<div class="stat-item pending">
|
||||
<span class="stat-value">${tasks.filter(t => t.status === 'pending').length}</span>
|
||||
<span class="stat-label">Pending</span>
|
||||
</div>
|
||||
<div class="stat-item failed">
|
||||
<span class="stat-value">${tasks.filter(t => t.status === 'failed').length}</span>
|
||||
<span class="stat-label">Failed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="queue-detail-groups">
|
||||
${Object.entries(grouped).map(([groupId, items]) => `
|
||||
<div class="queue-group-section">
|
||||
<div class="queue-group-header">
|
||||
<i data-lucide="folder" class="w-4 h-4"></i>
|
||||
<span>${groupId}</span>
|
||||
<span class="text-xs text-muted-foreground">(${items.length} tasks)</span>
|
||||
</div>
|
||||
<div class="queue-group-items">
|
||||
${items.map(item => `
|
||||
<div class="queue-detail-item ${item.status || ''}">
|
||||
<div class="item-main">
|
||||
<span class="item-id font-mono text-xs">${item.queue_id || item.task_id || 'N/A'}</span>
|
||||
<span class="item-title text-sm">${item.title || item.action || 'Untitled'}</span>
|
||||
</div>
|
||||
<div class="item-meta">
|
||||
<span class="item-issue text-xs">${item.issue_id || ''}</span>
|
||||
<span class="item-status ${item.status || ''}">${item.status || 'unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.querySelector('.issue-modal-body').innerHTML = detailHtml;
|
||||
lucide.createIcons();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to load queue detail:', err);
|
||||
modal.querySelector('.issue-modal-body').innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<button class="btn-sm btn-secondary mb-4" onclick="showQueueHistoryModal()">
|
||||
<i data-lucide="arrow-left" class="w-3 h-3"></i>
|
||||
Back
|
||||
</button>
|
||||
<div class="text-red-500">
|
||||
<i data-lucide="alert-circle" class="w-8 h-8 mx-auto mb-2"></i>
|
||||
<p>Failed to load queue detail</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function copyCommand(command) {
|
||||
navigator.clipboard.writeText(command).then(() => {
|
||||
showNotification(t('common.copied') || 'Copied to clipboard', 'success');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-code-workflow",
|
||||
"version": "6.3.8",
|
||||
"version": "6.3.9",
|
||||
"description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution",
|
||||
"type": "module",
|
||||
"main": "ccw/src/index.js",
|
||||
|
||||
Reference in New Issue
Block a user