Files
gh-duongdev-ccpm/commands/_shared-workflow-state.md
2025-11-29 18:24:24 +08:00

12 KiB

Shared Workflow State Detection

This file provides workflow state detection utilities used by the new natural workflow commands (plan, work, sync, commit, verify, done).

Purpose

Detect potential workflow issues before executing commands:

  • Uncommitted changes before creating new task
  • Stale sync (>2h) before starting work
  • Incomplete tasks before finalizing
  • Wrong branch before operations

Architecture

Linear Operations: This file delegates all Linear read operations to the linear-operations subagent for optimal token usage and caching.

Git Operations: All git-based state detection remains local in this file (no external dependencies).

Function Classification:

  • Linear read functions (use subagent): detectStaleSync(), checkTaskCompletion()
  • Pure git functions (local): detectUncommittedChanges(), detectActiveWork(), isBranchPushed()

State Detection Functions

1. Detect Uncommitted Changes

function detectUncommittedChanges() {
  try {
    const status = execSync('git status --porcelain', {
      encoding: 'utf-8'
    }).trim()

    if (status.length === 0) {
      return { hasChanges: false }
    }

    // Parse changes
    const lines = status.split('\n')
    const changes = lines.map(line => {
      const status = line.substring(0, 2)
      const path = line.substring(3)
      return { status: status.trim(), path }
    })

    return {
      hasChanges: true,
      count: changes.length,
      changes,
      summary: generateChangeSummary(changes)
    }
  } catch (error) {
    return { hasChanges: false, error: 'Not a git repository' }
  }
}

function generateChangeSummary(changes) {
  const modified = changes.filter(c => c.status === 'M').length
  const added = changes.filter(c => c.status === 'A' || c.status === '??').length
  const deleted = changes.filter(c => c.status === 'D').length

  const parts = []
  if (modified > 0) parts.push(`${modified} modified`)
  if (added > 0) parts.push(`${added} new`)
  if (deleted > 0) parts.push(`${deleted} deleted`)

  return parts.join(', ')
}

2. Detect Stale Sync

Uses Linear subagent to fetch issue comments, then compares with current time.

async function detectStaleSync(issueId) {
  try {
    // Step 1: Fetch issue with comments via Linear subagent
    const linearResult = await Task('linear-operations', `
operation: get_issue
params:
  issue_id: "${issueId}"
  include_comments: true
context:
  command: "workflow:detect-stale"
  purpose: "Checking if Linear comments are stale"
`);

    if (!linearResult.success) {
      return {
        isStale: false,
        error: linearResult.error?.message || 'Failed to fetch issue'
      }
    }

    const issue = linearResult.data
    const comments = issue.comments || []

    // Step 2: Find most recent sync comment (local logic)
    const syncComments = comments.filter(c =>
      c.body.includes('## 🔄 Progress Sync') ||
      c.body.includes('Progress Sync') ||
      c.body.includes('📝 Implementation Progress')
    )

    if (syncComments.length === 0) {
      return { isStale: false, reason: 'No previous sync' }
    }

    // Step 3: Compare timestamps (local logic)
    const lastSync = syncComments[syncComments.length - 1]
    const lastSyncTime = new Date(lastSync.createdAt)
    const now = new Date()
    const hoursSinceSync = (now - lastSyncTime) / (1000 * 60 * 60)

    return {
      isStale: hoursSinceSync > 2,
      hoursSinceSync: Math.round(hoursSinceSync * 10) / 10,
      lastSyncTime: lastSyncTime.toISOString()
    }
  } catch (error) {
    return { isStale: false, error: error.message }
  }
}

Note: The Linear subagent caches comments at session level, making subsequent calls very fast (<50ms). The parsing and comparison logic remains local for full control over stale detection thresholds.

3. Detect Active Work on Another Task

async function detectActiveWork(currentIssueId) {
  try {
    // Check git branch for different issue
    const branch = execSync('git rev-parse --abbrev-ref HEAD', {
      encoding: 'utf-8'
    }).trim()

    const branchMatch = branch.match(/([A-Z]+-\d+)/)
    if (branchMatch && branchMatch[1] !== currentIssueId) {
      return {
        hasActiveWork: true,
        activeIssueId: branchMatch[1],
        branch
      }
    }

    // Check for uncommitted work
    const uncommitted = detectUncommittedChanges()
    if (uncommitted.hasChanges) {
      return {
        hasActiveWork: true,
        uncommittedChanges: uncommitted
      }
    }

    return { hasActiveWork: false }
  } catch (error) {
    return { hasActiveWork: false, error: error.message }
  }
}

4. Check If Branch is Pushed

function isBranchPushed() {
  try {
    execSync('git rev-parse @{u}', { stdio: 'ignore' })
    return { isPushed: true }
  } catch {
    const branch = execSync('git rev-parse --abbrev-ref HEAD', {
      encoding: 'utf-8'
    }).trim()

    return {
      isPushed: false,
      branch,
      command: `git push -u origin ${branch}`
    }
  }
}

5. Check Task Completion Status

Uses Linear subagent to fetch issue description, then parses checklist locally.

async function checkTaskCompletion(issueId) {
  try {
    // Step 1: Fetch issue via Linear subagent
    const linearResult = await Task('linear-operations', `
operation: get_issue
params:
  issue_id: "${issueId}"
  include_comments: false
  include_attachments: false
context:
  command: "workflow:check-completion"
  purpose: "Checking task completion status from checklist"
`);

    if (!linearResult.success) {
      return {
        hasChecklist: false,
        isComplete: false,
        error: linearResult.error?.message || 'Failed to fetch issue'
      }
    }

    const issue = linearResult.data

    // Step 2: Parse checklist from description (local logic)
    const description = issue.description || ''
    const checklistMatch = description.match(/- \[([ x])\]/g)

    if (!checklistMatch) {
      return {
        hasChecklist: false,
        isComplete: true // No checklist = assume complete
      }
    }

    // Step 3: Calculate completion percentage (local logic)
    const total = checklistMatch.length
    const completed = checklistMatch.filter(m => m.includes('[x]')).length
    const percent = Math.round((completed / total) * 100)

    return {
      hasChecklist: true,
      isComplete: completed === total,
      total,
      completed,
      percent,
      remaining: total - completed
    }
  } catch (error) {
    return { hasChecklist: false, isComplete: false, error: error.message }
  }
}

Note: The Linear subagent caches issue descriptions at session level. The regex parsing and completion calculation remain local for full control over what constitutes "completion".

Usage in Commands

In /ccpm:plan (before creating new task)

// Check for active work before creating new task
const activeWork = await detectActiveWork(null)

if (activeWork.hasActiveWork) {
  console.log("⚠️  You have active work in progress")
  console.log("")

  if (activeWork.activeIssueId) {
    console.log(`Current branch: ${activeWork.branch}`)
    console.log(`Active issue: ${activeWork.activeIssueId}`)
  }

  if (activeWork.uncommittedChanges) {
    console.log(`Uncommitted changes: ${activeWork.uncommittedChanges.summary}`)
  }

  console.log("")
  console.log("Recommendation:")
  console.log("  1. Commit current work: /ccpm:commit")
  console.log("  2. Or sync progress: /ccpm:sync")
  console.log("  3. Then create new task")
  console.log("")

  // Ask user if they want to proceed anyway
  const answer = await askUser("Create new task anyway?")
  if (answer !== "Yes") {
    process.exit(0)
  }
}

In /ccpm:work (before starting work)

// Check for stale sync
const staleCheck = await detectStaleSync(issueId)

if (staleCheck.isStale) {
  console.log(`⚠️  Last sync was ${staleCheck.hoursSinceSync} hours ago`)
  console.log("")
  console.log("Recommendation: Sync progress first")
  console.log(`  /ccpm:sync ${issueId}`)
  console.log("")

  const answer = await askUser("Continue without syncing?")
  if (answer !== "Yes") {
    process.exit(0)
  }
}

In /ccpm:done (before finalizing)

// Check task completion
const completion = await checkTaskCompletion(issueId)

if (completion.hasChecklist && !completion.isComplete) {
  console.log(`⚠️  Task is only ${completion.percent}% complete`)
  console.log(`   ${completion.remaining} checklist items remaining`)
  console.log("")
  console.log("Recommendation: Complete all tasks first")
  console.log(`  /ccpm:work ${issueId}`)
  console.log("")

  const answer = await askUser("Finalize incomplete task?")
  if (answer !== "Yes") {
    process.exit(0)
  }
}

// Check if branch is pushed
const pushCheck = isBranchPushed()

if (!pushCheck.isPushed) {
  console.error("❌ Branch not pushed to remote")
  console.log("")
  console.log(`Push first: ${pushCheck.command}`)
  process.exit(1)
}

Warning Display Template

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️  Workflow Warning
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

${warningMessage}

${recommendation}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Subagent Integration

Linear Operations Subagent

Two functions in this file use the linear-operations subagent for optimized read operations:

  1. detectStaleSync(issueId)

    • Uses: linear-operations with get_issue operation
    • Fetches: Issue with comments (include_comments: true)
    • Local logic: Filters sync comments, compares timestamps
    • Performance: <50ms for cached calls, ~400-500ms for uncached
    • Caching: Session-level cache automatically populated
  2. checkTaskCompletion(issueId)

    • Uses: linear-operations with get_issue operation
    • Fetches: Issue description only (no comments/attachments)
    • Local logic: Regex parsing, completion calculation
    • Performance: <50ms for cached calls, ~400-500ms for uncached
    • Caching: Session-level cache automatically populated

Why Use the Subagent?

  • Token Efficiency: 60-70% fewer tokens vs direct Linear MCP calls
  • Caching: Session-level cache hits = massive performance boost
  • Consistency: Single source of truth for Linear API interactions
  • Error Handling: Standardized error responses with helpful suggestions
  • Maintainability: Linear API changes isolated to subagent

Error Handling

Both functions gracefully handle subagent errors:

if (!linearResult.success) {
  return {
    isStale: false,  // or appropriate default
    error: linearResult.error?.message || 'Fallback error message'
  }
}

If the subagent fails to fetch Linear data, the workflow continues with safe defaults rather than blocking.

Subagent Task Format

Both functions use the standard CCPM subagent invocation pattern:

const result = await Task('linear-operations', `
operation: <operation_name>
params:
  <param_name>: <value>
  ...
context:
  command: "workflow:..."
  purpose: "..."
`);

Key fields:

  • operation: The subagent operation (e.g., get_issue)
  • params: Operation parameters with issue_id/team/etc
  • context: Metadata for logging and command tracking
  • success: Result boolean indicating success/failure
  • data: Operation response (issue object, etc)
  • error: Error details if success=false
  • metadata: Execution metrics (duration_ms, mcp_calls, cached flag)

Benefits

Prevents Common Mistakes: Catches issues before they cause problems Actionable Recommendations: Always suggests what to do next User Control: Warnings, not errors - user can proceed if needed Context Aware: Different checks for different workflow stages Optimized Linear Reads: Uses subagent caching for 60-70% token reduction Pure Git Operations: All git logic remains fast and local