Files
Claude-Code-Workflow/.claude/commands/idaw/resume.md
catlog22 3463bc8e27 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.
2026-03-01 19:40:05 +08:00

13 KiB

name, description, argument-hint, allowed-tools
name description argument-hint allowed-tools
resume Resume interrupted IDAW session from last checkpoint [-y|--yes] [session-id] 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

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

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

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

// 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

// 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:

// 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

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

# 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