mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-02 15:23:19 +08:00
- 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.
457 lines
14 KiB
Markdown
457 lines
14 KiB
Markdown
---
|
|
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
|
|
```
|