mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-04 01:40:45 +08:00
feat: 增加队列状态“merged”及相关验证功能,添加状态参考文档
This commit is contained in:
@@ -25,9 +25,12 @@ Available CLI endpoints are dynamically defined by the config file:
|
|||||||
- **TaskOutput usage**: Only use `TaskOutput({ task_id: "xxx", block: false })` + sleep loop to poll completion status. NEVER read intermediate output during agent/CLI execution - wait for final result only
|
- **TaskOutput usage**: Only use `TaskOutput({ task_id: "xxx", block: false })` + sleep loop to poll completion status. NEVER read intermediate output during agent/CLI execution - wait for final result only
|
||||||
|
|
||||||
### CLI Tool Calls (ccw cli)
|
### CLI Tool Calls (ccw cli)
|
||||||
- **Default: `run_in_background: true`** - Unless otherwise specified, always use background execution for CLI calls:
|
- **Default: Use Bash `run_in_background: true`** - Unless otherwise specified, always execute CLI calls in background using Bash tool's background mode:
|
||||||
```
|
```
|
||||||
Bash({ command: "ccw cli -p '...' --tool gemini", run_in_background: true })
|
Bash({
|
||||||
|
command: "ccw cli -p '...' --tool gemini",
|
||||||
|
run_in_background: true // Bash tool parameter, not ccw cli parameter
|
||||||
|
})
|
||||||
```
|
```
|
||||||
- **After CLI call**: Stop output immediately - let CLI execute in background. **DO NOT use TaskOutput polling** - wait for hook callback to receive results
|
- **After CLI call**: Stop output immediately - let CLI execute in background. **DO NOT use TaskOutput polling** - wait for hook callback to receive results
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "E2E-TASK-1769007254162",
|
|
||||||
"title": "Test Task E2E-TASK-1769007254162",
|
|
||||||
"description": "Test task with loop control",
|
|
||||||
"status": "pending",
|
|
||||||
"loop_control": {
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Test loop",
|
|
||||||
"max_iterations": 3,
|
|
||||||
"success_condition": "current_iteration >= 3",
|
|
||||||
"error_policy": {
|
|
||||||
"on_failure": "pause",
|
|
||||||
"max_retries": 3
|
|
||||||
},
|
|
||||||
"cli_sequence": [
|
|
||||||
{
|
|
||||||
"step_id": "step1",
|
|
||||||
"tool": "bash",
|
|
||||||
"command": "echo \"iteration\""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"step_id": "step2",
|
|
||||||
"tool": "gemini",
|
|
||||||
"mode": "analysis",
|
|
||||||
"prompt_template": "Process output"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -172,7 +172,7 @@ interface ExecutionGroup {
|
|||||||
interface Queue {
|
interface Queue {
|
||||||
id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS (derived from filename)
|
id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS (derived from filename)
|
||||||
name?: string; // Optional queue name
|
name?: string; // Optional queue name
|
||||||
status: 'active' | 'completed' | 'archived' | 'failed';
|
status: 'active' | 'completed' | 'archived' | 'failed' | 'merged';
|
||||||
issue_ids: string[]; // Issues in this queue
|
issue_ids: string[]; // Issues in this queue
|
||||||
tasks: QueueItem[]; // Task items (task-level queue)
|
tasks: QueueItem[]; // Task items (task-level queue)
|
||||||
solutions?: QueueItem[]; // Solution items (solution-level queue)
|
solutions?: QueueItem[]; // Solution items (solution-level queue)
|
||||||
@@ -186,6 +186,8 @@ interface Queue {
|
|||||||
completed_count: number;
|
completed_count: number;
|
||||||
failed_count: number;
|
failed_count: number;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
merged_into?: string; // Queue ID this was merged into
|
||||||
|
merged_at?: string; // Timestamp of merge
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +233,37 @@ interface IssueOptions {
|
|||||||
|
|
||||||
const ISSUES_DIR = '.workflow/issues';
|
const ISSUES_DIR = '.workflow/issues';
|
||||||
|
|
||||||
|
// ============ Status Constants ============
|
||||||
|
|
||||||
|
const VALID_QUEUE_STATUSES = ['active', 'completed', 'archived', 'failed', 'merged'] as const;
|
||||||
|
const VALID_ITEM_STATUSES = ['pending', 'ready', 'executing', 'completed', 'failed', 'blocked'] as const;
|
||||||
|
const VALID_ISSUE_STATUSES = ['registered', 'planning', 'planned', 'queued', 'executing', 'completed', 'failed', 'paused'] as const;
|
||||||
|
|
||||||
|
type QueueStatus = typeof VALID_QUEUE_STATUSES[number];
|
||||||
|
type QueueItemStatus = typeof VALID_ITEM_STATUSES[number];
|
||||||
|
type IssueStatus = typeof VALID_ISSUE_STATUSES[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate queue status
|
||||||
|
*/
|
||||||
|
function validateQueueStatus(status: string): status is QueueStatus {
|
||||||
|
return VALID_QUEUE_STATUSES.includes(status as QueueStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate queue item status
|
||||||
|
*/
|
||||||
|
function validateItemStatus(status: string): status is QueueItemStatus {
|
||||||
|
return VALID_ITEM_STATUSES.includes(status as QueueItemStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate issue status
|
||||||
|
*/
|
||||||
|
function validateIssueStatus(status: string): status is IssueStatus {
|
||||||
|
return VALID_ISSUE_STATUSES.includes(status as IssueStatus);
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Storage Layer (JSONL) ============
|
// ============ Storage Layer (JSONL) ============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -811,7 +844,7 @@ function mergeQueues(target: Queue, source: Queue, options?: { deleteSource?: bo
|
|||||||
index.queues = index.queues.filter(q => q.id !== source.id);
|
index.queues = index.queues.filter(q => q.id !== source.id);
|
||||||
} else {
|
} else {
|
||||||
// Mark source queue as merged
|
// Mark source queue as merged
|
||||||
source.status = 'merged' as any;
|
source.status = 'merged';
|
||||||
if (!source._metadata) {
|
if (!source._metadata) {
|
||||||
source._metadata = {
|
source._metadata = {
|
||||||
version: '2.1',
|
version: '2.1',
|
||||||
@@ -823,8 +856,8 @@ function mergeQueues(target: Queue, source: Queue, options?: { deleteSource?: bo
|
|||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
(source._metadata as any).merged_into = target.id;
|
source._metadata.merged_into = target.id;
|
||||||
(source._metadata as any).merged_at = new Date().toISOString();
|
source._metadata.merged_at = new Date().toISOString();
|
||||||
writeQueue(source);
|
writeQueue(source);
|
||||||
|
|
||||||
const sourceEntry = index.queues.find(q => q.id === source.id);
|
const sourceEntry = index.queues.find(q => q.id === source.id);
|
||||||
@@ -1806,13 +1839,12 @@ async function updateAction(issueId: string | undefined, options: IssueOptions):
|
|||||||
const updates: Partial<Issue> = {};
|
const updates: Partial<Issue> = {};
|
||||||
|
|
||||||
if (options.status) {
|
if (options.status) {
|
||||||
const validStatuses = ['registered', 'planning', 'planned', 'queued', 'executing', 'completed', 'failed', 'paused'];
|
if (!validateIssueStatus(options.status)) {
|
||||||
if (!validStatuses.includes(options.status)) {
|
|
||||||
console.error(chalk.red(`Invalid status: ${options.status}`));
|
console.error(chalk.red(`Invalid status: ${options.status}`));
|
||||||
console.error(chalk.gray(`Valid: ${validStatuses.join(', ')}`));
|
console.error(chalk.gray(`Valid: ${VALID_ISSUE_STATUSES.join(', ')}`));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
updates.status = options.status as Issue['status'];
|
updates.status = options.status;
|
||||||
|
|
||||||
// Auto-set timestamps based on status
|
// Auto-set timestamps based on status
|
||||||
if (options.status === 'planned') updates.planned_at = new Date().toISOString();
|
if (options.status === 'planned') updates.planned_at = new Date().toISOString();
|
||||||
@@ -2437,15 +2469,31 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show current queue
|
// Show current queue - use readQueue() to detect if queue actually exists
|
||||||
const queue = readActiveQueue();
|
const queue = readQueue();
|
||||||
|
|
||||||
|
// Handle no active queue case for all output modes
|
||||||
|
if (!queue) {
|
||||||
|
if (options.brief || options.json) {
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
status: 'empty',
|
||||||
|
message: 'No active queue',
|
||||||
|
id: null,
|
||||||
|
issue_ids: [],
|
||||||
|
total: 0,
|
||||||
|
items: []
|
||||||
|
}, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Human-readable output handled below
|
||||||
|
}
|
||||||
|
|
||||||
// Brief mode: minimal queue info (id, issue_ids, item summaries)
|
// Brief mode: minimal queue info (id, issue_ids, item summaries)
|
||||||
if (options.brief) {
|
if (options.brief) {
|
||||||
const items = queue.solutions || queue.tasks || [];
|
const items = queue!.solutions || queue!.tasks || [];
|
||||||
const briefQueue = {
|
const briefQueue = {
|
||||||
id: queue.id,
|
id: queue!.id,
|
||||||
issue_ids: queue.issue_ids || [],
|
issue_ids: queue!.issue_ids || [],
|
||||||
total: items.length,
|
total: items.length,
|
||||||
pending: items.filter(i => i.status === 'pending').length,
|
pending: items.filter(i => i.status === 'pending').length,
|
||||||
executing: items.filter(i => i.status === 'executing').length,
|
executing: items.filter(i => i.status === 'executing').length,
|
||||||
@@ -2469,14 +2517,21 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
|||||||
|
|
||||||
console.log(chalk.bold.cyan('\nActive Queue\n'));
|
console.log(chalk.bold.cyan('\nActive Queue\n'));
|
||||||
|
|
||||||
|
// Handle no queue case (human-readable)
|
||||||
|
if (!queue) {
|
||||||
|
console.log(chalk.yellow('No active queue'));
|
||||||
|
console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
|
||||||
|
console.log(chalk.gray('Or list history: ccw issue queue list'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Support both solution-level and task-level queues
|
// Support both solution-level and task-level queues
|
||||||
const items = queue.solutions || queue.tasks || [];
|
const items = queue.solutions || queue.tasks || [];
|
||||||
const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
|
const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
|
||||||
|
|
||||||
if (!queue.id || items.length === 0) {
|
if (items.length === 0) {
|
||||||
console.log(chalk.yellow('No active queue'));
|
console.log(chalk.yellow(`Queue ${queue.id} is empty`));
|
||||||
console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
|
console.log(chalk.gray('Add issues: ccw issue queue add <issue-id>'));
|
||||||
console.log(chalk.gray('Or list history: ccw issue queue list'));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2908,10 +2963,10 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
|
|||||||
created_at: item.failure_details.timestamp
|
created_at: item.failure_details.timestamp
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keep issue status as 'failed' (or optionally 'pending_replan')
|
// Reset issue status to 'queued' for re-execution
|
||||||
// This signals to planning phase that this issue had failures
|
// Failure details preserved in feedback for debugging
|
||||||
updateIssue(item.issue_id, {
|
updateIssue(item.issue_id, {
|
||||||
status: 'failed',
|
status: 'queued',
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2927,7 +2982,7 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
|
|||||||
item.failure_history.push(item.failure_details);
|
item.failure_history.push(item.failure_details);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset QueueItem for retry (but Issue status remains 'failed')
|
// Reset QueueItem for retry (Issue status also reset to 'queued')
|
||||||
item.status = 'pending';
|
item.status = 'pending';
|
||||||
item.failure_reason = undefined;
|
item.failure_reason = undefined;
|
||||||
item.failure_details = undefined;
|
item.failure_details = undefined;
|
||||||
|
|||||||
194
status-reference.md
Normal file
194
status-reference.md
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
# Issue System Status Reference
|
||||||
|
|
||||||
|
> CCW Issue Management 状态管理参考手册
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
CCW Issue 系统包含三个核心实体,每个实体有独立的状态定义:
|
||||||
|
|
||||||
|
| 实体 | 描述 | 存储位置 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **Issue** | 问题/任务 | `.workflow/issues/issues.jsonl` |
|
||||||
|
| **Queue** | 执行队列 | `.workflow/issues/queues/*.json` |
|
||||||
|
| **QueueItem** | 队列项(解决方案级别) | Queue 内的 `solutions[]` 数组 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 状态
|
||||||
|
|
||||||
|
**类型定义**: `'registered' | 'planning' | 'planned' | 'queued' | 'executing' | 'completed' | 'failed' | 'paused'`
|
||||||
|
|
||||||
|
| 状态 | 含义 | 触发命令/场景 |
|
||||||
|
|------|------|---------------|
|
||||||
|
| `registered` | 已注册,待规划 | `ccw issue create`, `ccw issue init`, `ccw issue pull` |
|
||||||
|
| `planning` | 规划中 | 手动设置 `ccw issue update <id> --status planning` |
|
||||||
|
| `planned` | 已规划,solution 已绑定 | `ccw issue bind <issue-id> <solution-id>` |
|
||||||
|
| `queued` | 已加入执行队列 | `ccw issue queue add <issue-id>`, `ccw issue retry` |
|
||||||
|
| `executing` | 执行中 | `ccw issue next` |
|
||||||
|
| `completed` | 已完成 | `ccw issue done <item-id>` |
|
||||||
|
| `failed` | 执行失败 | `ccw issue done <item-id> --fail` |
|
||||||
|
| `paused` | 暂停 | 手动设置 `ccw issue update <id> --status paused` |
|
||||||
|
|
||||||
|
### Issue 状态流转图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ ▼
|
||||||
|
registered ──→ planning ──→ planned ──→ queued ──→ executing ──→ completed
|
||||||
|
▲ │
|
||||||
|
│ ▼
|
||||||
|
└─────── failed
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
paused (手动)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Queue 状态
|
||||||
|
|
||||||
|
**类型定义**: `'active' | 'completed' | 'archived' | 'failed' | 'merged'`
|
||||||
|
|
||||||
|
| 状态 | 含义 | 触发命令/场景 |
|
||||||
|
|------|------|---------------|
|
||||||
|
| `active` | 活跃队列,可执行 | 创建队列时默认, `ccw issue retry` 重置 |
|
||||||
|
| `completed` | 已完成(所有项完成) | 所有 QueueItem 状态为 `completed` |
|
||||||
|
| `archived` | 已归档 | `ccw issue queue archive` |
|
||||||
|
| `failed` | 有失败项 | 有 QueueItem 失败且所有项已终止 |
|
||||||
|
| `merged` | 已合并到其他队列 | `ccw issue queue merge <source> --queue <target>` |
|
||||||
|
|
||||||
|
### Queue 状态流转图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ ▼
|
||||||
|
active ───────┼──────────────────────────────────→ completed ──→ archived
|
||||||
|
│
|
||||||
|
│ ┌───────────┐
|
||||||
|
└────────→│ failed │
|
||||||
|
└─────┬─────┘
|
||||||
|
│ retry
|
||||||
|
▼
|
||||||
|
active
|
||||||
|
|
||||||
|
active ──────────────────────────────────────────→ merged
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## QueueItem 状态
|
||||||
|
|
||||||
|
**类型定义**: `'pending' | 'ready' | 'executing' | 'completed' | 'failed' | 'blocked'`
|
||||||
|
|
||||||
|
| 状态 | 含义 | 触发命令/场景 |
|
||||||
|
|------|------|---------------|
|
||||||
|
| `pending` | 等待执行 | 创建队列项时默认, `ccw issue retry` 重置 |
|
||||||
|
| `ready` | 依赖满足,可执行 | 计算得出(非持久化),用于 DAG |
|
||||||
|
| `executing` | 执行中 | `ccw issue next` |
|
||||||
|
| `completed` | 已完成 | `ccw issue done <item-id>` |
|
||||||
|
| `failed` | 失败 | `ccw issue done <item-id> --fail` |
|
||||||
|
| `blocked` | 被依赖阻塞 | 计算得出(非持久化),用于 DAG |
|
||||||
|
|
||||||
|
### QueueItem 状态流转图
|
||||||
|
|
||||||
|
```
|
||||||
|
pending ──→ executing ──→ completed
|
||||||
|
▲ │
|
||||||
|
│ ▼
|
||||||
|
└───────── failed (retry 重置)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**: `ready` 和 `blocked` 是在 `ccw issue queue dag` 命令中动态计算的,不会持久化存储。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 状态验证
|
||||||
|
|
||||||
|
系统内置状态验证函数,防止无效状态赋值:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 常量定义
|
||||||
|
const VALID_QUEUE_STATUSES = ['active', 'completed', 'archived', 'failed', 'merged'] as const;
|
||||||
|
const VALID_ITEM_STATUSES = ['pending', 'ready', 'executing', 'completed', 'failed', 'blocked'] as const;
|
||||||
|
const VALID_ISSUE_STATUSES = ['registered', 'planning', 'planned', 'queued', 'executing', 'completed', 'failed', 'paused'] as const;
|
||||||
|
|
||||||
|
// 验证函数
|
||||||
|
validateQueueStatus(status: string): status is QueueStatus
|
||||||
|
validateItemStatus(status: string): status is QueueItemStatus
|
||||||
|
validateIssueStatus(status: string): status is IssueStatus
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常用命令速查
|
||||||
|
|
||||||
|
### 状态查询
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看所有 Issue
|
||||||
|
ccw issue list
|
||||||
|
|
||||||
|
# 按状态筛选 Issue
|
||||||
|
ccw issue list --status planned,queued
|
||||||
|
|
||||||
|
# 查看队列状态
|
||||||
|
ccw issue queue
|
||||||
|
|
||||||
|
# 查看队列 DAG(含 ready/blocked 计算状态)
|
||||||
|
ccw issue queue dag
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态更新
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新 Issue 状态
|
||||||
|
ccw issue update <issue-id> --status <status>
|
||||||
|
|
||||||
|
# 从队列同步 Issue 状态为 queued
|
||||||
|
ccw issue update --from-queue
|
||||||
|
|
||||||
|
# 重试失败项(QueueItem → pending, Issue → queued, Queue → active)
|
||||||
|
ccw issue retry [issue-id]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 执行流程
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 添加到队列 (Issue → queued, 创建 QueueItem → pending)
|
||||||
|
ccw issue queue add <issue-id>
|
||||||
|
|
||||||
|
# 获取下一个执行项 (QueueItem → executing, Issue → executing)
|
||||||
|
ccw issue next
|
||||||
|
|
||||||
|
# 标记完成 (QueueItem → completed, Issue → completed)
|
||||||
|
ccw issue done <item-id>
|
||||||
|
|
||||||
|
# 标记失败 (QueueItem → failed, Issue → failed)
|
||||||
|
ccw issue done <item-id> --fail --reason "error message"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 状态对照表
|
||||||
|
|
||||||
|
| 操作 | Issue 状态 | Queue 状态 | QueueItem 状态 |
|
||||||
|
|------|-----------|------------|----------------|
|
||||||
|
| 创建 Issue | `registered` | - | - |
|
||||||
|
| 绑定 Solution | `planned` | - | - |
|
||||||
|
| 加入队列 | `queued` | `active` | `pending` |
|
||||||
|
| 开始执行 | `executing` | `active` | `executing` |
|
||||||
|
| 执行完成 | `completed` | `completed`* | `completed` |
|
||||||
|
| 执行失败 | `failed` | `failed`* | `failed` |
|
||||||
|
| 重试 | `queued` | `active` | `pending` |
|
||||||
|
| 归档队列 | - | `archived` | - |
|
||||||
|
| 合并队列 | - | `merged` | - |
|
||||||
|
|
||||||
|
> *Queue 状态在所有项完成/失败后才会更新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [Queue 命令详解](./queue.md)
|
||||||
|
- [Execute 工作流](./execute.md)
|
||||||
|
- [Plan 规划流程](./plan.md)
|
||||||
Reference in New Issue
Block a user