Initial commit
This commit is contained in:
424
commands/_shared-workflow-state.md
Normal file
424
commands/_shared-workflow-state.md
Normal file
@@ -0,0 +1,424 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user