425 lines
12 KiB
Markdown
425 lines
12 KiB
Markdown
# 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
|
|
|
|
```javascript
|
|
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.
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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.
|
|
|
|
```javascript
|
|
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)
|
|
|
|
```javascript
|
|
// 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)
|
|
|
|
```javascript
|
|
// 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)
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```markdown
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
⚠️ 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:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
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
|