mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-02 15:23:19 +08:00
feat(idaw): add resume, run, and status commands for task management
- Implemented /idaw:resume to resume interrupted sessions with task handling and auto mode. - Created /idaw:run for executing task skill chains with git checkpoints and session management. - Added /idaw:status for viewing task and session progress, including overview and specific session details. - Introduced helper functions for task type inference and skill argument assembly. - Enhanced task management with session tracking, progress reporting, and error handling.
This commit is contained in:
456
.claude/commands/idaw/run.md
Normal file
456
.claude/commands/idaw/run.md
Normal file
@@ -0,0 +1,456 @@
|
||||
---
|
||||
name: run
|
||||
description: IDAW orchestrator - execute task skill chains serially with git checkpoints
|
||||
argument-hint: "[-y|--yes] [--task <id>[,<id>,...]] [--dry-run]"
|
||||
allowed-tools: Skill(*), TodoWrite(*), AskUserQuestion(*), Read(*), Write(*), Bash(*), Glob(*)
|
||||
---
|
||||
|
||||
# IDAW Run Command (/idaw:run)
|
||||
|
||||
## Auto Mode
|
||||
|
||||
When `--yes` or `-y`: Skip all confirmations, auto-skip on failure, proceed with dirty git.
|
||||
|
||||
## Skill Chain Mapping
|
||||
|
||||
```javascript
|
||||
const SKILL_CHAIN_MAP = {
|
||||
'bugfix': ['workflow-lite-plan', 'workflow-test-fix'],
|
||||
'bugfix-hotfix': ['workflow-lite-plan'],
|
||||
'feature': ['workflow-lite-plan', 'workflow-test-fix'],
|
||||
'feature-complex': ['workflow-plan', 'workflow-execute', 'workflow-test-fix'],
|
||||
'refactor': ['workflow:refactor-cycle'],
|
||||
'tdd': ['workflow-tdd-plan', 'workflow-execute'],
|
||||
'test': ['workflow-test-fix'],
|
||||
'test-fix': ['workflow-test-fix'],
|
||||
'review': ['review-cycle'],
|
||||
'docs': ['workflow-lite-plan']
|
||||
};
|
||||
```
|
||||
|
||||
## Task Type Inference
|
||||
|
||||
```javascript
|
||||
function inferTaskType(title, description) {
|
||||
const text = `${title} ${description}`.toLowerCase();
|
||||
if (/urgent|production|critical/.test(text) && /fix|bug/.test(text)) return 'bugfix-hotfix';
|
||||
if (/refactor|重构|tech.*debt/.test(text)) return 'refactor';
|
||||
if (/tdd|test-driven|test first/.test(text)) return 'tdd';
|
||||
if (/test fail|fix test|failing test/.test(text)) return 'test-fix';
|
||||
if (/generate test|写测试|add test/.test(text)) return 'test';
|
||||
if (/review|code review/.test(text)) return 'review';
|
||||
if (/docs|documentation|readme/.test(text)) return 'docs';
|
||||
if (/fix|bug|error|crash|fail/.test(text)) return 'bugfix';
|
||||
if (/complex|multi-module|architecture/.test(text)) return 'feature-complex';
|
||||
return 'feature';
|
||||
}
|
||||
```
|
||||
|
||||
## 6-Phase Execution
|
||||
|
||||
### Phase 1: Load Tasks
|
||||
|
||||
```javascript
|
||||
const args = $ARGUMENTS;
|
||||
const autoYes = /(-y|--yes)/.test(args);
|
||||
const dryRun = /--dry-run/.test(args);
|
||||
const taskFilter = args.match(/--task\s+([\w,-]+)/)?.[1]?.split(',') || null;
|
||||
|
||||
// Load task files
|
||||
const taskFiles = Glob('.workflow/.idaw/tasks/IDAW-*.json') || [];
|
||||
|
||||
if (taskFiles.length === 0) {
|
||||
console.log('No IDAW tasks found. Use /idaw:add to create tasks.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse and filter
|
||||
let tasks = taskFiles.map(f => JSON.parse(Read(f)));
|
||||
|
||||
if (taskFilter) {
|
||||
tasks = tasks.filter(t => taskFilter.includes(t.id));
|
||||
} else {
|
||||
tasks = tasks.filter(t => t.status === 'pending');
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
console.log('No pending tasks to execute. Use /idaw:add to add tasks or --task to specify IDs.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort: priority ASC (1=critical first), then ID ASC
|
||||
tasks.sort((a, b) => {
|
||||
if (a.priority !== b.priority) return a.priority - b.priority;
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
```
|
||||
|
||||
### Phase 2: Session Setup
|
||||
|
||||
```javascript
|
||||
// Generate session ID: IDA-{slug}-YYYYMMDD
|
||||
const slug = tasks[0].title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.substring(0, 20)
|
||||
.replace(/-$/, '');
|
||||
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
let sessionId = `IDA-${slug}-${dateStr}`;
|
||||
|
||||
// Check collision
|
||||
const existingSession = Glob(`.workflow/.idaw/sessions/${sessionId}/session.json`);
|
||||
if (existingSession?.length > 0) {
|
||||
sessionId = `${sessionId}-2`;
|
||||
}
|
||||
|
||||
const sessionDir = `.workflow/.idaw/sessions/${sessionId}`;
|
||||
Bash(`mkdir -p "${sessionDir}"`);
|
||||
|
||||
const session = {
|
||||
session_id: sessionId,
|
||||
status: 'running',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
tasks: tasks.map(t => t.id),
|
||||
current_task: null,
|
||||
completed: [],
|
||||
failed: [],
|
||||
skipped: []
|
||||
};
|
||||
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
// Initialize progress.md
|
||||
const progressHeader = `# IDAW Progress — ${sessionId}\nStarted: ${session.created_at}\n\n`;
|
||||
Write(`${sessionDir}/progress.md`, progressHeader);
|
||||
|
||||
// TodoWrite
|
||||
TodoWrite({
|
||||
todos: tasks.map((t, i) => ({
|
||||
content: `IDAW:[${i + 1}/${tasks.length}] ${t.title}`,
|
||||
status: i === 0 ? 'in_progress' : 'pending',
|
||||
activeForm: `Executing ${t.title}`
|
||||
}))
|
||||
});
|
||||
```
|
||||
|
||||
### Phase 3: Startup Protocol
|
||||
|
||||
```javascript
|
||||
// Check for existing running sessions
|
||||
const runningSessions = Glob('.workflow/.idaw/sessions/IDA-*/session.json')
|
||||
?.map(f => JSON.parse(Read(f)))
|
||||
.filter(s => s.status === 'running' && s.session_id !== sessionId) || [];
|
||||
|
||||
if (runningSessions.length > 0) {
|
||||
if (!autoYes) {
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: `Found running session: ${runningSessions[0].session_id}. How to proceed?`,
|
||||
header: 'Conflict',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Resume existing', description: 'Use /idaw:resume instead' },
|
||||
{ label: 'Start fresh', description: 'Continue with new session' },
|
||||
{ label: 'Abort', description: 'Cancel this run' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
if (answer.answers?.Conflict === 'Resume existing') {
|
||||
console.log(`Use: /idaw:resume ${runningSessions[0].session_id}`);
|
||||
return;
|
||||
}
|
||||
if (answer.answers?.Conflict === 'Abort') return;
|
||||
}
|
||||
// autoYes or "Start fresh": proceed
|
||||
}
|
||||
|
||||
// Check git status
|
||||
const gitStatus = Bash('git status --porcelain 2>/dev/null');
|
||||
if (gitStatus?.trim()) {
|
||||
if (!autoYes) {
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: 'Working tree has uncommitted changes. How to proceed?',
|
||||
header: 'Git',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Continue', description: 'Proceed with dirty tree' },
|
||||
{ label: 'Stash', description: 'git stash before running' },
|
||||
{ label: 'Abort', description: 'Stop and handle manually' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
if (answer.answers?.Git === 'Stash') {
|
||||
Bash('git stash push -m "idaw-pre-run"');
|
||||
}
|
||||
if (answer.answers?.Git === 'Abort') return;
|
||||
}
|
||||
// autoYes: proceed silently
|
||||
}
|
||||
|
||||
// Dry run: show plan and exit
|
||||
if (dryRun) {
|
||||
console.log(`# Dry Run — ${sessionId}\n`);
|
||||
for (const task of tasks) {
|
||||
const taskType = task.task_type || inferTaskType(task.title, task.description);
|
||||
const chain = task.skill_chain || SKILL_CHAIN_MAP[taskType] || SKILL_CHAIN_MAP['feature'];
|
||||
console.log(`## ${task.id}: ${task.title}`);
|
||||
console.log(` Type: ${taskType} | Priority: ${task.priority}`);
|
||||
console.log(` Chain: ${chain.join(' → ')}\n`);
|
||||
}
|
||||
console.log(`Total: ${tasks.length} tasks`);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Main Loop (serial, one task at a time)
|
||||
|
||||
```javascript
|
||||
for (let taskIdx = 0; taskIdx < tasks.length; taskIdx++) {
|
||||
const task = tasks[taskIdx];
|
||||
|
||||
// Skip completed/failed/skipped
|
||||
if (['completed', 'failed', 'skipped'].includes(task.status)) continue;
|
||||
|
||||
// Resolve skill chain
|
||||
const resolvedType = task.task_type || inferTaskType(task.title, task.description);
|
||||
const chain = task.skill_chain || SKILL_CHAIN_MAP[resolvedType] || SKILL_CHAIN_MAP['feature'];
|
||||
|
||||
// Update task status → in_progress
|
||||
task.status = 'in_progress';
|
||||
task.task_type = resolvedType; // persist inferred type
|
||||
task.execution.started_at = new Date().toISOString();
|
||||
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||
|
||||
// Update session
|
||||
session.current_task = task.id;
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
console.log(`\n--- [${taskIdx + 1}/${tasks.length}] ${task.id}: ${task.title} ---`);
|
||||
console.log(`Chain: ${chain.join(' → ')}`);
|
||||
|
||||
// Execute each skill in chain
|
||||
let previousResult = null;
|
||||
let taskFailed = false;
|
||||
|
||||
for (let skillIdx = 0; skillIdx < chain.length; skillIdx++) {
|
||||
const skillName = chain[skillIdx];
|
||||
const skillArgs = assembleSkillArgs(skillName, task, previousResult, autoYes, skillIdx === 0);
|
||||
|
||||
console.log(` [${skillIdx + 1}/${chain.length}] ${skillName}`);
|
||||
|
||||
try {
|
||||
const result = Skill({ skill: skillName, args: skillArgs });
|
||||
previousResult = result;
|
||||
task.execution.skill_results.push({
|
||||
skill: skillName,
|
||||
status: 'completed',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
// Retry once
|
||||
console.log(` Retry: ${skillName} (first attempt failed)`);
|
||||
try {
|
||||
const retryResult = Skill({ skill: skillName, args: skillArgs });
|
||||
previousResult = retryResult;
|
||||
task.execution.skill_results.push({
|
||||
skill: skillName,
|
||||
status: 'completed-retry',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (retryError) {
|
||||
// Failed after retry
|
||||
task.execution.skill_results.push({
|
||||
skill: skillName,
|
||||
status: 'failed',
|
||||
error: String(retryError).substring(0, 200),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (autoYes) {
|
||||
taskFailed = true;
|
||||
break;
|
||||
} else {
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: `${skillName} failed: ${String(retryError).substring(0, 100)}. How to proceed?`,
|
||||
header: 'Error',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Skip task', description: 'Mark task as failed, continue to next' },
|
||||
{ label: 'Abort', description: 'Stop entire run' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
if (answer.answers?.Error === 'Abort') {
|
||||
task.status = 'failed';
|
||||
task.execution.error = String(retryError).substring(0, 200);
|
||||
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||
session.failed.push(task.id);
|
||||
session.status = 'failed';
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
return;
|
||||
}
|
||||
taskFailed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Checkpoint (per task) — inline
|
||||
if (taskFailed) {
|
||||
task.status = 'failed';
|
||||
task.execution.error = 'Skill chain failed after retry';
|
||||
task.execution.completed_at = new Date().toISOString();
|
||||
session.failed.push(task.id);
|
||||
} else {
|
||||
// Git commit checkpoint
|
||||
const commitMsg = `feat(idaw): ${task.title} [${task.id}]`;
|
||||
const diffCheck = Bash('git diff --stat HEAD 2>/dev/null || echo ""');
|
||||
const untrackedCheck = Bash('git ls-files --others --exclude-standard 2>/dev/null || echo ""');
|
||||
|
||||
if (diffCheck?.trim() || untrackedCheck?.trim()) {
|
||||
Bash('git add -A');
|
||||
const commitResult = Bash(`git commit -m "$(cat <<'EOF'\n${commitMsg}\nEOF\n)"`);
|
||||
const commitHash = Bash('git rev-parse --short HEAD 2>/dev/null')?.trim();
|
||||
task.execution.git_commit = commitHash;
|
||||
} else {
|
||||
task.execution.git_commit = 'no-commit';
|
||||
}
|
||||
|
||||
task.status = 'completed';
|
||||
task.execution.completed_at = new Date().toISOString();
|
||||
session.completed.push(task.id);
|
||||
}
|
||||
|
||||
// Write task + session state
|
||||
task.updated_at = new Date().toISOString();
|
||||
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
// Append to progress.md
|
||||
const duration = task.execution.started_at && task.execution.completed_at
|
||||
? formatDuration(new Date(task.execution.completed_at) - new Date(task.execution.started_at))
|
||||
: 'unknown';
|
||||
|
||||
const progressEntry = `## ${task.id} — ${task.title}\n` +
|
||||
`- Status: ${task.status}\n` +
|
||||
`- Type: ${task.task_type}\n` +
|
||||
`- Chain: ${chain.join(' → ')}\n` +
|
||||
`- Commit: ${task.execution.git_commit || '-'}\n` +
|
||||
`- Duration: ${duration}\n\n`;
|
||||
|
||||
const currentProgress = Read(`${sessionDir}/progress.md`);
|
||||
Write(`${sessionDir}/progress.md`, currentProgress + progressEntry);
|
||||
|
||||
// Update TodoWrite
|
||||
if (taskIdx + 1 < tasks.length) {
|
||||
TodoWrite({
|
||||
todos: tasks.map((t, i) => ({
|
||||
content: `IDAW:[${i + 1}/${tasks.length}] ${t.title}`,
|
||||
status: i < taskIdx + 1 ? 'completed' : (i === taskIdx + 1 ? 'in_progress' : 'pending'),
|
||||
activeForm: `Executing ${t.title}`
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 6: Report
|
||||
|
||||
```javascript
|
||||
session.status = session.failed.length > 0 && session.completed.length === 0 ? 'failed' : 'completed';
|
||||
session.current_task = null;
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
// Final progress summary
|
||||
const summary = `\n---\n## Summary\n` +
|
||||
`- Completed: ${session.completed.length}\n` +
|
||||
`- Failed: ${session.failed.length}\n` +
|
||||
`- Skipped: ${session.skipped.length}\n` +
|
||||
`- Total: ${tasks.length}\n`;
|
||||
|
||||
const finalProgress = Read(`${sessionDir}/progress.md`);
|
||||
Write(`${sessionDir}/progress.md`, finalProgress + summary);
|
||||
|
||||
// Display report
|
||||
console.log('\n=== IDAW Run Complete ===');
|
||||
console.log(`Session: ${sessionId}`);
|
||||
console.log(`Completed: ${session.completed.length}/${tasks.length}`);
|
||||
if (session.failed.length > 0) console.log(`Failed: ${session.failed.join(', ')}`);
|
||||
if (session.skipped.length > 0) console.log(`Skipped: ${session.skipped.join(', ')}`);
|
||||
|
||||
// List git commits
|
||||
for (const taskId of session.completed) {
|
||||
const t = JSON.parse(Read(`.workflow/.idaw/tasks/${taskId}.json`));
|
||||
if (t.execution.git_commit && t.execution.git_commit !== 'no-commit') {
|
||||
console.log(` ${t.execution.git_commit} ${t.title}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
### assembleSkillArgs
|
||||
|
||||
```javascript
|
||||
function assembleSkillArgs(skillName, task, previousResult, autoYes, isFirst) {
|
||||
let args = '';
|
||||
|
||||
if (isFirst) {
|
||||
// First skill: pass task goal
|
||||
const goal = `${task.title}\n${task.description}`;
|
||||
args = `"${goal.replace(/"/g, '\\"')}"`;
|
||||
|
||||
// bugfix-hotfix: add --hotfix
|
||||
if (task.task_type === 'bugfix-hotfix') {
|
||||
args += ' --hotfix';
|
||||
}
|
||||
} else if (previousResult?.session_id) {
|
||||
// Subsequent skills: chain session
|
||||
args = `--session="${previousResult.session_id}"`;
|
||||
}
|
||||
|
||||
// Propagate -y
|
||||
if (autoYes && !args.includes('-y') && !args.includes('--yes')) {
|
||||
args = args ? `${args} -y` : '-y';
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
```
|
||||
|
||||
### formatDuration
|
||||
|
||||
```javascript
|
||||
function formatDuration(ms) {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
if (minutes > 0) return `${minutes}m ${remainingSeconds}s`;
|
||||
return `${seconds}s`;
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Execute all pending tasks
|
||||
/idaw:run -y
|
||||
|
||||
# Execute specific tasks
|
||||
/idaw:run --task IDAW-001,IDAW-003
|
||||
|
||||
# Dry run (show plan without executing)
|
||||
/idaw:run --dry-run
|
||||
|
||||
# Interactive mode (confirm at each step)
|
||||
/idaw:run
|
||||
```
|
||||
Reference in New Issue
Block a user