mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
Refactor issue management and history tracking
- Removed the interactive issue management command and its associated documentation. - Enhanced the issue planning command to streamline project context reading and solution creation. - Improved queue management with conflict clarification and status syncing from queues. - Added functionality to track completed issues, moving them to a history file upon completion. - Updated CLI options to support syncing issue statuses from queues. - Introduced new API endpoint for retrieving completed issues from history. - Enhanced error handling and validation for issue updates and queue management.
This commit is contained in:
@@ -282,9 +282,11 @@ export function run(argv: string[]): void {
|
||||
// New options for solution/queue management
|
||||
.option('--solution <path>', 'Solution JSON file path')
|
||||
.option('--solution-id <id>', 'Solution ID')
|
||||
.option('--data <json>', 'JSON data for create/solution')
|
||||
.option('--result <json>', 'Execution result JSON')
|
||||
.option('--reason <text>', 'Failure reason')
|
||||
.option('--fail', 'Mark task as failed')
|
||||
.option('--from-queue [queue-id]', 'Sync issue statuses from queue (default: active queue)')
|
||||
.action((subcommand, args, options) => issueCommand(subcommand, args, options));
|
||||
|
||||
program.parse(argv);
|
||||
|
||||
@@ -196,6 +196,7 @@ interface IssueOptions {
|
||||
fail?: boolean;
|
||||
ids?: boolean; // List only IDs (one per line)
|
||||
data?: string; // JSON data for create
|
||||
fromQueue?: boolean | string; // Sync statuses from queue (true=active, string=specific queue ID)
|
||||
}
|
||||
|
||||
const ISSUES_DIR = '.workflow/issues';
|
||||
@@ -251,12 +252,69 @@ function findIssue(issueId: string): Issue | undefined {
|
||||
return readIssues().find(i => i.id === issueId);
|
||||
}
|
||||
|
||||
// ============ Issue History JSONL ============
|
||||
|
||||
function readIssueHistory(): Issue[] {
|
||||
const path = join(getIssuesDir(), 'issue-history.jsonl');
|
||||
if (!existsSync(path)) return [];
|
||||
try {
|
||||
return readFileSync(path, 'utf-8')
|
||||
.split('\n')
|
||||
.filter(line => line.trim())
|
||||
.map(line => JSON.parse(line));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function appendIssueHistory(issue: Issue): void {
|
||||
ensureIssuesDir();
|
||||
const path = join(getIssuesDir(), 'issue-history.jsonl');
|
||||
const line = JSON.stringify(issue) + '\n';
|
||||
// Append to history file
|
||||
if (existsSync(path)) {
|
||||
const content = readFileSync(path, 'utf-8');
|
||||
// Ensure proper newline before appending
|
||||
const needsNewline = content.length > 0 && !content.endsWith('\n');
|
||||
writeFileSync(path, (needsNewline ? '\n' : '') + line, { flag: 'a' });
|
||||
} else {
|
||||
writeFileSync(path, line, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move completed issue from issues.jsonl to issue-history.jsonl
|
||||
*/
|
||||
function moveIssueToHistory(issueId: string): boolean {
|
||||
const issues = readIssues();
|
||||
const idx = issues.findIndex(i => i.id === issueId);
|
||||
if (idx === -1) return false;
|
||||
|
||||
const issue = issues[idx];
|
||||
if (issue.status !== 'completed') return false;
|
||||
|
||||
// Append to history
|
||||
appendIssueHistory(issue);
|
||||
|
||||
// Remove from active issues
|
||||
issues.splice(idx, 1);
|
||||
writeIssues(issues);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateIssue(issueId: string, updates: Partial<Issue>): boolean {
|
||||
const issues = readIssues();
|
||||
const idx = issues.findIndex(i => i.id === issueId);
|
||||
if (idx === -1) return false;
|
||||
issues[idx] = { ...issues[idx], ...updates, updated_at: new Date().toISOString() };
|
||||
writeIssues(issues);
|
||||
|
||||
// Auto-move to history when completed
|
||||
if (updates.status === 'completed') {
|
||||
moveIssueToHistory(issueId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -740,6 +798,46 @@ async function listAction(issueId: string | undefined, options: IssueOptions): P
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* history - List completed issues from history
|
||||
*/
|
||||
async function historyAction(options: IssueOptions): Promise<void> {
|
||||
const history = readIssueHistory();
|
||||
|
||||
// IDs only mode
|
||||
if (options.ids) {
|
||||
history.forEach(i => console.log(i.id));
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(history, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (history.length === 0) {
|
||||
console.log(chalk.yellow('No completed issues in history'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold.cyan('\nIssue History (Completed)\n'));
|
||||
console.log(chalk.gray('ID'.padEnd(25) + 'Completed At'.padEnd(22) + 'Title'));
|
||||
console.log(chalk.gray('-'.repeat(80)));
|
||||
|
||||
for (const issue of history) {
|
||||
const completedAt = issue.completed_at
|
||||
? new Date(issue.completed_at).toLocaleString()
|
||||
: 'N/A';
|
||||
console.log(
|
||||
chalk.green(issue.id.padEnd(25)) +
|
||||
completedAt.padEnd(22) +
|
||||
(issue.title || '').substring(0, 35)
|
||||
);
|
||||
}
|
||||
|
||||
console.log(chalk.gray(`\nTotal: ${history.length} completed issues`));
|
||||
}
|
||||
|
||||
/**
|
||||
* status - Show detailed status
|
||||
*/
|
||||
@@ -891,11 +989,88 @@ async function taskAction(issueId: string | undefined, taskId: string | undefine
|
||||
|
||||
/**
|
||||
* update - Update issue fields (status, priority, title, etc.)
|
||||
* --from-queue: Sync statuses from active queue (auto-update queued issues)
|
||||
*/
|
||||
async function updateAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
|
||||
// Handle --from-queue: Sync statuses from queue
|
||||
if (options.fromQueue) {
|
||||
// Determine queue ID: string value = specific queue, true = active queue
|
||||
const queueId = typeof options.fromQueue === 'string' ? options.fromQueue : undefined;
|
||||
const queue = queueId ? readQueue(queueId) : readActiveQueue();
|
||||
|
||||
if (!queue) {
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({ success: false, message: `Queue not found: ${queueId}`, queued: [], unplanned: [] }));
|
||||
} else {
|
||||
console.log(chalk.red(`Queue not found: ${queueId}`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const items = queue.solutions || queue.tasks || [];
|
||||
const allIssues = readIssues();
|
||||
|
||||
if (!queue.id || items.length === 0) {
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({ success: false, message: 'No active queue', queued: [], unplanned: [] }));
|
||||
} else {
|
||||
console.log(chalk.yellow('No active queue to sync from'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get issue IDs from queue
|
||||
const queuedIssueIds = new Set(items.map(item => item.issue_id));
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Track updates
|
||||
const updated: string[] = [];
|
||||
const unplanned: string[] = [];
|
||||
|
||||
// Update queued issues
|
||||
for (const issueId of queuedIssueIds) {
|
||||
const issue = allIssues.find(i => i.id === issueId);
|
||||
if (issue && issue.status !== 'queued' && issue.status !== 'executing' && issue.status !== 'completed') {
|
||||
updateIssue(issueId, { status: 'queued', queued_at: now });
|
||||
updated.push(issueId);
|
||||
}
|
||||
}
|
||||
|
||||
// Find planned issues NOT in queue
|
||||
for (const issue of allIssues) {
|
||||
if (issue.status === 'planned' && issue.bound_solution_id && !queuedIssueIds.has(issue.id)) {
|
||||
unplanned.push(issue.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({
|
||||
success: true,
|
||||
queue_id: queue.id,
|
||||
queued: updated,
|
||||
queued_count: updated.length,
|
||||
unplanned: unplanned,
|
||||
unplanned_count: unplanned.length
|
||||
}, null, 2));
|
||||
} else {
|
||||
console.log(chalk.green(`✓ Synced from queue ${queue.id}`));
|
||||
console.log(chalk.gray(` Updated to 'queued': ${updated.length} issues`));
|
||||
if (updated.length > 0) {
|
||||
updated.forEach(id => console.log(chalk.gray(` - ${id}`)));
|
||||
}
|
||||
if (unplanned.length > 0) {
|
||||
console.log(chalk.yellow(` Planned but NOT in queue: ${unplanned.length} issues`));
|
||||
unplanned.forEach(id => console.log(chalk.yellow(` - ${id}`)));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard single-issue update
|
||||
if (!issueId) {
|
||||
console.error(chalk.red('Issue ID is required'));
|
||||
console.error(chalk.gray('Usage: ccw issue update <issue-id> --status <status> [--priority <n>] [--title "..."]'));
|
||||
console.error(chalk.gray('Usage: ccw issue update <issue-id> --status <status>'));
|
||||
console.error(chalk.gray(' ccw issue update --from-queue [queue-id] (sync from queue)'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -1712,6 +1887,9 @@ export async function issueCommand(
|
||||
case 'list':
|
||||
await listAction(argsArray[0], options);
|
||||
break;
|
||||
case 'history':
|
||||
await historyAction(options);
|
||||
break;
|
||||
case 'status':
|
||||
await statusAction(argsArray[0], options);
|
||||
break;
|
||||
@@ -1753,12 +1931,15 @@ export async function issueCommand(
|
||||
default:
|
||||
console.log(chalk.bold.cyan('\nCCW Issue Management (v3.0 - Multi-Queue + Lifecycle)\n'));
|
||||
console.log(chalk.bold('Core Commands:'));
|
||||
console.log(chalk.gray(' init <issue-id> Initialize new issue'));
|
||||
console.log(chalk.gray(' create --data \'{"title":"..."}\' Create issue (auto-generates ID)'));
|
||||
console.log(chalk.gray(' init <issue-id> Initialize new issue (manual ID)'));
|
||||
console.log(chalk.gray(' list [issue-id] List issues or tasks'));
|
||||
console.log(chalk.gray(' history List completed issues (from history)'));
|
||||
console.log(chalk.gray(' status [issue-id] Show detailed status'));
|
||||
console.log(chalk.gray(' task <issue-id> [task-id] Add or update task'));
|
||||
console.log(chalk.gray(' bind <issue-id> [sol-id] Bind solution (--solution <path> to register)'));
|
||||
console.log(chalk.gray(' update <issue-id> Update issue (--status, --priority, --title)'));
|
||||
console.log(chalk.gray(' solution <id> --data \'{...}\' Create solution (auto-generates ID)'));
|
||||
console.log(chalk.gray(' bind <issue-id> [sol-id] Bind solution'));
|
||||
console.log(chalk.gray(' update <issue-id> --status <s> Update issue status'));
|
||||
console.log(chalk.gray(' update --from-queue [queue-id] Sync statuses from queue (default: active)'));
|
||||
console.log();
|
||||
console.log(chalk.bold('Queue Commands:'));
|
||||
console.log(chalk.gray(' queue Show active queue'));
|
||||
@@ -1787,7 +1968,8 @@ export async function issueCommand(
|
||||
console.log(chalk.gray(' --force Force operation'));
|
||||
console.log();
|
||||
console.log(chalk.bold('Storage:'));
|
||||
console.log(chalk.gray(' .workflow/issues/issues.jsonl All issues'));
|
||||
console.log(chalk.gray(' .workflow/issues/issues.jsonl Active issues'));
|
||||
console.log(chalk.gray(' .workflow/issues/issue-history.jsonl Completed issues'));
|
||||
console.log(chalk.gray(' .workflow/issues/solutions/*.jsonl Solutions per issue'));
|
||||
console.log(chalk.gray(' .workflow/issues/queues/ Queue files (multi-queue)'));
|
||||
console.log(chalk.gray(' .workflow/issues/queues/index.json Queue index'));
|
||||
|
||||
@@ -67,6 +67,17 @@ function readSolutionsJsonl(issuesDir: string, issueId: string): any[] {
|
||||
}
|
||||
}
|
||||
|
||||
function readIssueHistoryJsonl(issuesDir: string): any[] {
|
||||
const historyPath = join(issuesDir, 'issue-history.jsonl');
|
||||
if (!existsSync(historyPath)) return [];
|
||||
try {
|
||||
const content = readFileSync(historyPath, 'utf8');
|
||||
return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeSolutionsJsonl(issuesDir: string, issueId: string, solutions: any[]) {
|
||||
const solutionsDir = join(issuesDir, 'solutions');
|
||||
if (!existsSync(solutionsDir)) mkdirSync(solutionsDir, { recursive: true });
|
||||
@@ -376,6 +387,17 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/issues/history - List completed issues from history
|
||||
if (pathname === '/api/issues/history' && req.method === 'GET') {
|
||||
const history = readIssueHistoryJsonl(issuesDir);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
issues: history,
|
||||
_metadata: { version: '1.0', storage: 'jsonl', total_issues: history.length, last_updated: new Date().toISOString() }
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/issues - Create issue
|
||||
if (pathname === '/api/issues' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
|
||||
Reference in New Issue
Block a user