diff --git a/.claude/commands/idaw/add.md b/.claude/commands/idaw/add.md new file mode 100644 index 00000000..78c14f5d --- /dev/null +++ b/.claude/commands/idaw/add.md @@ -0,0 +1,273 @@ +--- +name: add +description: Add IDAW tasks - manual creation or import from ccw issue +argument-hint: "[-y|--yes] [--from-issue [,,...]] \"description\" [--type ] [--priority <1-5>]" +allowed-tools: AskUserQuestion(*), Read(*), Bash(*), Write(*), Glob(*) +--- + +# IDAW Add Command (/idaw:add) + +## Auto Mode + +When `--yes` or `-y`: Skip clarification questions, create task with inferred details. + +## IDAW Task Schema + +```json +{ + "id": "IDAW-001", + "title": "string", + "description": "string", + "status": "pending", + "priority": 2, + "task_type": null, + "skill_chain": null, + "context": { + "affected_files": [], + "acceptance_criteria": [], + "constraints": [], + "references": [] + }, + "source": { + "type": "manual|import-issue", + "issue_id": null, + "issue_snapshot": null + }, + "execution": { + "session_id": null, + "started_at": null, + "completed_at": null, + "skill_results": [], + "git_commit": null, + "error": null + }, + "created_at": "ISO", + "updated_at": "ISO" +} +``` + +**Valid task_type values**: `bugfix|bugfix-hotfix|feature|feature-complex|refactor|tdd|test|test-fix|review|docs` + +## Implementation + +### Phase 1: Parse Arguments + +```javascript +const args = $ARGUMENTS; +const autoYes = /(-y|--yes)/.test(args); +const fromIssue = args.match(/--from-issue\s+([\w,-]+)/)?.[1]; +const typeFlag = args.match(/--type\s+([\w-]+)/)?.[1]; +const priorityFlag = args.match(/--priority\s+(\d)/)?.[1]; +const description = args.replace(/(-y|--yes|--from-issue\s+[\w,-]+|--type\s+[\w-]+|--priority\s+\d)/g, '').trim().replace(/^["']|["']$/g, ''); +``` + +### Phase 2: Route — Import or Manual + +``` +--from-issue present? + ├─ YES → Import Mode (Phase 3A) + └─ NO → Manual Mode (Phase 3B) +``` + +### Phase 3A: Import Mode (from ccw issue) + +```javascript +const issueIds = fromIssue.split(','); + +for (const issueId of issueIds) { + // 1. Fetch issue data + const issueJson = Bash(`ccw issue list --json`); + const issues = JSON.parse(issueJson).issues; + const issue = issues.find(i => i.id === issueId.trim()); + if (!issue) { + console.log(`Warning: Issue ${issueId} not found, skipping`); + continue; + } + + // 2. Check duplicate (same issue_id already imported) + const existing = Glob('.workflow/.idaw/tasks/IDAW-*.json'); + for (const f of existing) { + const data = JSON.parse(Read(f)); + if (data.source?.issue_id === issueId.trim()) { + console.log(`Warning: Issue ${issueId} already imported as ${data.id}, skipping`); + continue; // skip to next issue + } + } + + // 3. Generate next IDAW ID + const nextId = generateNextId(); + + // 4. Map issue → IDAW task + const task = { + id: nextId, + title: issue.title, + description: issue.context || issue.title, + status: 'pending', + priority: parseInt(priorityFlag) || issue.priority || 3, + task_type: typeFlag || inferTaskType(issue.title, issue.context || ''), + skill_chain: null, + context: { + affected_files: issue.affected_components || [], + acceptance_criteria: [], + constraints: [], + references: issue.source_url ? [issue.source_url] : [] + }, + source: { + type: 'import-issue', + issue_id: issue.id, + issue_snapshot: { + id: issue.id, + title: issue.title, + status: issue.status, + context: issue.context, + priority: issue.priority, + created_at: issue.created_at + } + }, + execution: { + session_id: null, + started_at: null, + completed_at: null, + skill_results: [], + git_commit: null, + error: null + }, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + }; + + // 5. Write task file + Bash('mkdir -p .workflow/.idaw/tasks'); + Write(`.workflow/.idaw/tasks/${nextId}.json`, JSON.stringify(task, null, 2)); + console.log(`Created ${nextId} from issue ${issueId}: ${issue.title}`); +} +``` + +### Phase 3B: Manual Mode + +```javascript +// 1. Validate description +if (!description && !autoYes) { + const answer = AskUserQuestion({ + questions: [{ + question: 'Please provide a task description:', + header: 'Task', + multiSelect: false, + options: [ + { label: 'Provide description', description: 'What needs to be done?' } + ] + }] + }); + // Use custom text from "Other" + description = answer.customText || ''; +} + +if (!description) { + console.log('Error: No description provided. Usage: /idaw:add "task description"'); + return; +} + +// 2. Generate next IDAW ID +const nextId = generateNextId(); + +// 3. Build title from first sentence +const title = description.split(/[.\n]/)[0].substring(0, 80).trim(); + +// 4. Determine task_type +const taskType = typeFlag || null; // null → inferred at run time + +// 5. Create task +const task = { + id: nextId, + title: title, + description: description, + status: 'pending', + priority: parseInt(priorityFlag) || 3, + task_type: taskType, + skill_chain: null, + context: { + affected_files: [], + acceptance_criteria: [], + constraints: [], + references: [] + }, + source: { + type: 'manual', + issue_id: null, + issue_snapshot: null + }, + execution: { + session_id: null, + started_at: null, + completed_at: null, + skill_results: [], + git_commit: null, + error: null + }, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() +}; + +Bash('mkdir -p .workflow/.idaw/tasks'); +Write(`.workflow/.idaw/tasks/${nextId}.json`, JSON.stringify(task, null, 2)); +console.log(`Created ${nextId}: ${title}`); +``` + +## Helper Functions + +### ID Generation + +```javascript +function generateNextId() { + const files = Glob('.workflow/.idaw/tasks/IDAW-*.json') || []; + if (files.length === 0) return 'IDAW-001'; + + const maxNum = files + .map(f => parseInt(f.match(/IDAW-(\d+)/)?.[1] || '0')) + .reduce((max, n) => Math.max(max, n), 0); + + return `IDAW-${String(maxNum + 1).padStart(3, '0')}`; +} +``` + +### Task Type Inference (deferred — used at run time if task_type is null) + +```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'; +} +``` + +## Examples + +```bash +# Manual creation +/idaw:add "Fix login timeout bug" --type bugfix --priority 2 +/idaw:add "Add rate limiting to API endpoints" --priority 1 +/idaw:add "Refactor auth module to use strategy pattern" + +# Import from ccw issue +/idaw:add --from-issue ISS-20260128-001 +/idaw:add --from-issue ISS-20260128-001,ISS-20260128-002 --priority 1 + +# Auto mode (skip clarification) +/idaw:add -y "Quick fix for typo in header" +``` + +## Output + +``` +Created IDAW-001: Fix login timeout bug + Type: bugfix | Priority: 2 | Source: manual + → Next: /idaw:run or /idaw:status +``` diff --git a/.claude/commands/idaw/resume.md b/.claude/commands/idaw/resume.md new file mode 100644 index 00000000..371b3518 --- /dev/null +++ b/.claude/commands/idaw/resume.md @@ -0,0 +1,398 @@ +--- +name: resume +description: Resume interrupted IDAW session from last checkpoint +argument-hint: "[-y|--yes] [session-id]" +allowed-tools: Skill(*), TodoWrite(*), AskUserQuestion(*), Read(*), Write(*), Bash(*), Glob(*) +--- + +# IDAW Resume Command (/idaw:resume) + +## Auto Mode + +When `--yes` or `-y`: Auto-skip interrupted task, continue with remaining. + +## 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'; +} +``` + +## Implementation + +### Phase 1: Find Resumable Session + +```javascript +const args = $ARGUMENTS; +const autoYes = /(-y|--yes)/.test(args); +const targetSessionId = args.replace(/(-y|--yes)/g, '').trim(); + +let session = null; +let sessionDir = null; + +if (targetSessionId) { + // Load specific session + sessionDir = `.workflow/.idaw/sessions/${targetSessionId}`; + try { + session = JSON.parse(Read(`${sessionDir}/session.json`)); + } catch { + console.log(`Session "${targetSessionId}" not found.`); + console.log('Use /idaw:status to list sessions, or /idaw:run to start a new one.'); + return; + } +} else { + // Find most recent running session + const sessionFiles = Glob('.workflow/.idaw/sessions/IDA-*/session.json') || []; + + for (const f of sessionFiles) { + try { + const s = JSON.parse(Read(f)); + if (s.status === 'running') { + session = s; + sessionDir = f.replace(/\/session\.json$/, '').replace(/\\session\.json$/, ''); + break; + } + } catch { + // Skip malformed + } + } + + if (!session) { + console.log('No running sessions found to resume.'); + console.log('Use /idaw:run to start a new execution.'); + return; + } +} + +console.log(`Resuming session: ${session.session_id}`); +``` + +### Phase 2: Handle Interrupted Task + +```javascript +// Find the task that was in_progress when interrupted +let currentTaskId = session.current_task; +let currentTask = null; + +if (currentTaskId) { + try { + currentTask = JSON.parse(Read(`.workflow/.idaw/tasks/${currentTaskId}.json`)); + } catch { + console.log(`Warning: Could not read task ${currentTaskId}`); + currentTaskId = null; + } +} + +if (currentTask && currentTask.status === 'in_progress') { + if (autoYes) { + // Auto: skip interrupted task + currentTask.status = 'skipped'; + currentTask.execution.error = 'Skipped on resume (auto mode)'; + currentTask.execution.completed_at = new Date().toISOString(); + currentTask.updated_at = new Date().toISOString(); + Write(`.workflow/.idaw/tasks/${currentTaskId}.json`, JSON.stringify(currentTask, null, 2)); + session.skipped.push(currentTaskId); + console.log(`Skipped interrupted task: ${currentTaskId}`); + } else { + const answer = AskUserQuestion({ + questions: [{ + question: `Task ${currentTaskId} was interrupted: "${currentTask.title}". How to proceed?`, + header: 'Resume', + multiSelect: false, + options: [ + { label: 'Retry', description: 'Reset to pending, re-execute from beginning' }, + { label: 'Skip', description: 'Mark as skipped, move to next task' } + ] + }] + }); + + if (answer.answers?.Resume === 'Skip') { + currentTask.status = 'skipped'; + currentTask.execution.error = 'Skipped on resume (user choice)'; + currentTask.execution.completed_at = new Date().toISOString(); + currentTask.updated_at = new Date().toISOString(); + Write(`.workflow/.idaw/tasks/${currentTaskId}.json`, JSON.stringify(currentTask, null, 2)); + session.skipped.push(currentTaskId); + } else { + // Retry: reset to pending + currentTask.status = 'pending'; + currentTask.execution.started_at = null; + currentTask.execution.completed_at = null; + currentTask.execution.skill_results = []; + currentTask.execution.error = null; + currentTask.updated_at = new Date().toISOString(); + Write(`.workflow/.idaw/tasks/${currentTaskId}.json`, JSON.stringify(currentTask, null, 2)); + } + } +} +``` + +### Phase 3: Build Remaining Task Queue + +```javascript +// Collect remaining tasks (pending, or the retried current task) +const allTaskIds = session.tasks; +const completedSet = new Set([...session.completed, ...session.failed, ...session.skipped]); + +const remainingTasks = []; +for (const taskId of allTaskIds) { + if (completedSet.has(taskId)) continue; + try { + const task = JSON.parse(Read(`.workflow/.idaw/tasks/${taskId}.json`)); + if (task.status === 'pending') { + remainingTasks.push(task); + } + } catch { + console.log(`Warning: Could not read task ${taskId}, skipping`); + } +} + +if (remainingTasks.length === 0) { + console.log('No remaining tasks to execute. Session complete.'); + session.status = 'completed'; + session.current_task = null; + session.updated_at = new Date().toISOString(); + Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2)); + return; +} + +// Sort: priority ASC, then ID ASC +remainingTasks.sort((a, b) => { + if (a.priority !== b.priority) return a.priority - b.priority; + return a.id.localeCompare(b.id); +}); + +console.log(`Remaining tasks: ${remainingTasks.length}`); + +// Append resume marker to progress.md +const progressFile = `${sessionDir}/progress.md`; +try { + const currentProgress = Read(progressFile); + Write(progressFile, currentProgress + `\n---\n**Resumed**: ${new Date().toISOString()}\n\n`); +} catch { + Write(progressFile, `# IDAW Progress — ${session.session_id}\n\n---\n**Resumed**: ${new Date().toISOString()}\n\n`); +} + +// Update TodoWrite +TodoWrite({ + todos: remainingTasks.map((t, i) => ({ + content: `IDAW:[${i + 1}/${remainingTasks.length}] ${t.title}`, + status: i === 0 ? 'in_progress' : 'pending', + activeForm: `Executing ${t.title}` + })) +}); +``` + +### Phase 4-6: Execute Remaining (reuse run.md main loop) + +Execute remaining tasks using the same Phase 4-6 logic from `/idaw:run`: + +```javascript +// Phase 4: Main Loop — identical to run.md Phase 4 +for (let taskIdx = 0; taskIdx < remainingTasks.length; taskIdx++) { + const task = remainingTasks[taskIdx]; + + // 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 → in_progress + task.status = 'in_progress'; + task.task_type = resolvedType; + task.execution.started_at = new Date().toISOString(); + Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2)); + + 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}/${remainingTasks.length}] ${task.id}: ${task.title} ---`); + console.log(`Chain: ${chain.join(' → ')}`); + + // Execute skill 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); + + 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 + 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) { + task.execution.skill_results.push({ + skill: skillName, + status: 'failed', + error: String(retryError).substring(0, 200), + timestamp: new Date().toISOString() + }); + + if (autoYes) { + taskFailed = true; + break; + } + + const answer = AskUserQuestion({ + questions: [{ + question: `${skillName} failed: ${String(retryError).substring(0, 100)}`, + header: 'Error', + multiSelect: false, + options: [ + { label: 'Skip task', description: 'Mark as failed, continue' }, + { label: 'Abort', description: 'Stop 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 + 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 + 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'); + 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); + } + + 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 progress + const chain_str = chain.join(' → '); + const progressEntry = `## ${task.id} — ${task.title}\n- Status: ${task.status}\n- Chain: ${chain_str}\n- Commit: ${task.execution.git_commit || '-'}\n\n`; + const currentProgress = Read(`${sessionDir}/progress.md`); + Write(`${sessionDir}/progress.md`, currentProgress + progressEntry); +} + +// Phase 6: Report +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)); + +const summary = `\n---\n## Summary (Resumed)\n- Completed: ${session.completed.length}\n- Failed: ${session.failed.length}\n- Skipped: ${session.skipped.length}\n`; +const finalProgress = Read(`${sessionDir}/progress.md`); +Write(`${sessionDir}/progress.md`, finalProgress + summary); + +console.log('\n=== IDAW Resume Complete ==='); +console.log(`Session: ${session.session_id}`); +console.log(`Completed: ${session.completed.length} | Failed: ${session.failed.length} | Skipped: ${session.skipped.length}`); +``` + +## Helper Functions + +### assembleSkillArgs + +```javascript +function assembleSkillArgs(skillName, task, previousResult, autoYes, isFirst) { + let args = ''; + + if (isFirst) { + const goal = `${task.title}\n${task.description}`; + args = `"${goal.replace(/"/g, '\\"')}"`; + if (task.task_type === 'bugfix-hotfix') args += ' --hotfix'; + } else if (previousResult?.session_id) { + args = `--session="${previousResult.session_id}"`; + } + + if (autoYes && !args.includes('-y') && !args.includes('--yes')) { + args = args ? `${args} -y` : '-y'; + } + + return args; +} +``` + +## Examples + +```bash +# Resume most recent running session (interactive) +/idaw:resume + +# Resume specific session +/idaw:resume IDA-auth-fix-20260301 + +# Resume with auto mode (skip interrupted, continue) +/idaw:resume -y + +# Resume specific session with auto mode +/idaw:resume -y IDA-auth-fix-20260301 +``` diff --git a/.claude/commands/idaw/run.md b/.claude/commands/idaw/run.md new file mode 100644 index 00000000..c641b554 --- /dev/null +++ b/.claude/commands/idaw/run.md @@ -0,0 +1,456 @@ +--- +name: run +description: IDAW orchestrator - execute task skill chains serially with git checkpoints +argument-hint: "[-y|--yes] [--task [,,...]] [--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 +``` diff --git a/.claude/commands/idaw/status.md b/.claude/commands/idaw/status.md new file mode 100644 index 00000000..b551b577 --- /dev/null +++ b/.claude/commands/idaw/status.md @@ -0,0 +1,182 @@ +--- +name: status +description: View IDAW task and session progress +argument-hint: "[session-id]" +allowed-tools: Read(*), Glob(*), Bash(*) +--- + +# IDAW Status Command (/idaw:status) + +## Overview + +Read-only command to view IDAW task queue and execution session progress. + +## Implementation + +### Phase 1: Determine View Mode + +```javascript +const sessionId = $ARGUMENTS?.trim(); + +if (sessionId) { + // Specific session view + showSession(sessionId); +} else { + // Overview: pending tasks + latest session + showOverview(); +} +``` + +### Phase 2: Show Overview + +```javascript +function showOverview() { + // 1. Load all tasks + 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; + } + + const tasks = taskFiles.map(f => JSON.parse(Read(f))); + + // 2. Group by status + const byStatus = { + pending: tasks.filter(t => t.status === 'pending'), + in_progress: tasks.filter(t => t.status === 'in_progress'), + completed: tasks.filter(t => t.status === 'completed'), + failed: tasks.filter(t => t.status === 'failed'), + skipped: tasks.filter(t => t.status === 'skipped') + }; + + // 3. Display task summary table + console.log('# IDAW Tasks\n'); + console.log('| ID | Title | Type | Priority | Status |'); + console.log('|----|-------|------|----------|--------|'); + + // Sort: priority ASC, then ID ASC + const sorted = [...tasks].sort((a, b) => { + if (a.priority !== b.priority) return a.priority - b.priority; + return a.id.localeCompare(b.id); + }); + + for (const t of sorted) { + const type = t.task_type || '(infer)'; + console.log(`| ${t.id} | ${t.title.substring(0, 40)} | ${type} | ${t.priority} | ${t.status} |`); + } + + console.log(`\nTotal: ${tasks.length} | Pending: ${byStatus.pending.length} | Completed: ${byStatus.completed.length} | Failed: ${byStatus.failed.length}`); + + // 4. Show latest session (if any) + const sessionDirs = Glob('.workflow/.idaw/sessions/IDA-*/session.json') || []; + if (sessionDirs.length > 0) { + // Sort by modification time (newest first) — Glob returns sorted by mtime + const latestSessionFile = sessionDirs[0]; + const session = JSON.parse(Read(latestSessionFile)); + console.log(`\n## Latest Session: ${session.session_id}`); + console.log(`Status: ${session.status} | Tasks: ${session.tasks?.length || 0}`); + console.log(`Completed: ${session.completed?.length || 0} | Failed: ${session.failed?.length || 0} | Skipped: ${session.skipped?.length || 0}`); + } +} +``` + +### Phase 3: Show Specific Session + +```javascript +function showSession(sessionId) { + const sessionFile = `.workflow/.idaw/sessions/${sessionId}/session.json`; + const progressFile = `.workflow/.idaw/sessions/${sessionId}/progress.md`; + + // Try reading session + try { + const session = JSON.parse(Read(sessionFile)); + + console.log(`# IDAW Session: ${session.session_id}\n`); + console.log(`Status: ${session.status}`); + console.log(`Created: ${session.created_at}`); + console.log(`Updated: ${session.updated_at}`); + console.log(`Current Task: ${session.current_task || 'none'}\n`); + + // Task detail table + console.log('| ID | Title | Status | Commit |'); + console.log('|----|-------|--------|--------|'); + + for (const taskId of session.tasks) { + const taskFile = `.workflow/.idaw/tasks/${taskId}.json`; + try { + const task = JSON.parse(Read(taskFile)); + const commit = task.execution?.git_commit?.substring(0, 7) || '-'; + console.log(`| ${task.id} | ${task.title.substring(0, 40)} | ${task.status} | ${commit} |`); + } catch { + console.log(`| ${taskId} | (file not found) | unknown | - |`); + } + } + + console.log(`\nCompleted: ${session.completed?.length || 0} | Failed: ${session.failed?.length || 0} | Skipped: ${session.skipped?.length || 0}`); + + // Show progress.md if exists + try { + const progress = Read(progressFile); + console.log('\n---\n'); + console.log(progress); + } catch { + // No progress file yet + } + + } catch { + // Session not found — try listing all sessions + console.log(`Session "${sessionId}" not found.\n`); + listSessions(); + } +} +``` + +### Phase 4: List All Sessions + +```javascript +function listSessions() { + const sessionFiles = Glob('.workflow/.idaw/sessions/IDA-*/session.json') || []; + + if (sessionFiles.length === 0) { + console.log('No IDAW sessions found. Use /idaw:run to start execution.'); + return; + } + + console.log('# IDAW Sessions\n'); + console.log('| Session ID | Status | Tasks | Completed | Failed |'); + console.log('|------------|--------|-------|-----------|--------|'); + + for (const f of sessionFiles) { + try { + const session = JSON.parse(Read(f)); + console.log(`| ${session.session_id} | ${session.status} | ${session.tasks?.length || 0} | ${session.completed?.length || 0} | ${session.failed?.length || 0} |`); + } catch { + // Skip malformed + } + } + + console.log('\nUse /idaw:status for details.'); +} +``` + +## Examples + +```bash +# Show overview (pending tasks + latest session) +/idaw:status + +# Show specific session details +/idaw:status IDA-auth-fix-20260301 + +# Output example: +# IDAW Tasks +# +# | ID | Title | Type | Priority | Status | +# |----------|------------------------------------|--------|----------|-----------| +# | IDAW-001 | Fix auth token refresh | bugfix | 1 | completed | +# | IDAW-002 | Add rate limiting | feature| 2 | pending | +# | IDAW-003 | Refactor payment module | refact | 3 | pending | +# +# Total: 3 | Pending: 2 | Completed: 1 | Failed: 0 +``` diff --git a/docs/public/icon-concepts.html b/docs/public/icon-concepts.html index 302710d7..d7c685b1 100644 --- a/docs/public/icon-concepts.html +++ b/docs/public/icon-concepts.html @@ -2800,22 +2800,22 @@ - - + + - - + + - - + + @@ -2840,22 +2840,22 @@ - - + + - - + + - - + +