25 KiB
Shared Decision Helper Functions (Always-Ask Policy Implementation)
This file provides reusable decision-making utilities for implementing the Always-Ask Policy across all CCPM commands. These helpers enable consistent, confidence-based decision making with automatic user interaction when confidence is below thresholds.
Overview
The Always-Ask Policy states: When confidence < 80%, explicitly ask the user rather than making assumptions.
These helper functions provide:
- Confidence calculation - Score decisions 0-100 based on context
- Ask/proceed logic - Automatically ask user when confidence is low
- User question formatting - Standardized question templates
- Fuzzy matching - Intelligent string matching with thresholds
- Validation - Pre/during/post command validation
Usage in commands: Reference this file at the start of command execution:
READ: commands/_shared-decision-helpers.md
Then use the functions as described below.
Core Functions
1. calculateConfidence
Calculates confidence score (0-100) for a decision based on multiple signals.
/**
* Calculate confidence score for a decision
* @param {Object} context - Decision context
* @param {any} context.input - User input to evaluate
* @param {any} context.expected - Expected value/pattern
* @param {Object} context.signals - Additional confidence signals
* @param {number} context.signals.patternMatch - Pattern match confidence (0-100)
* @param {number} context.signals.contextMatch - Context match confidence (0-100)
* @param {number} context.signals.historicalSuccess - Historical success rate (0-100)
* @param {number} context.signals.userPreference - User preference strength (0-100)
* @returns {Object} Confidence result with score and reasoning
*/
function calculateConfidence(context) {
let confidence = 0;
const reasoning = [];
// Signal 1: Pattern Match (weight: 50%)
if (context.signals?.patternMatch !== undefined) {
const patternScore = context.signals.patternMatch * 0.5;
confidence += patternScore;
reasoning.push(`Pattern match: ${context.signals.patternMatch}% (weighted: ${patternScore.toFixed(0)})`);
}
// Signal 2: Context Match (weight: 30%)
if (context.signals?.contextMatch !== undefined) {
const contextScore = context.signals.contextMatch * 0.3;
confidence += contextScore;
reasoning.push(`Context match: ${context.signals.contextMatch}% (weighted: ${contextScore.toFixed(0)})`);
}
// Signal 3: Historical Success (weight: 20%)
if (context.signals?.historicalSuccess !== undefined) {
const historyScore = context.signals.historicalSuccess * 0.2;
confidence += historyScore;
reasoning.push(`Historical success: ${context.signals.historicalSuccess}% (weighted: ${historyScore.toFixed(0)})`);
}
// Signal 4: User Preference (bonus: +10)
if (context.signals?.userPreference !== undefined && context.signals.userPreference > 70) {
const preferenceBonus = 10;
confidence += preferenceBonus;
reasoning.push(`User preference bonus: +${preferenceBonus}`);
}
// Cap at 100
confidence = Math.min(confidence, 100);
return {
confidence: Math.round(confidence),
reasoning: reasoning.join(', '),
shouldAsk: confidence < 80,
level: getConfidenceLevel(confidence)
};
}
function getConfidenceLevel(confidence) {
if (confidence >= 95) return 'CERTAIN';
if (confidence >= 80) return 'HIGH';
if (confidence >= 50) return 'MEDIUM';
return 'LOW';
}
Usage Example:
// Calculate confidence for issue ID pattern
const result = calculateConfidence({
input: "PSN-29",
expected: /^[A-Z]+-\d+$/,
signals: {
patternMatch: 100, // Exact regex match
contextMatch: 90, // Branch name contains PSN-29
historicalSuccess: 95 // 95% past success rate
}
});
// Result:
// {
// confidence: 93,
// reasoning: "Pattern match: 100% (weighted: 50), Context match: 90% (weighted: 27), Historical success: 95% (weighted: 19)",
// shouldAsk: false,
// level: "HIGH"
// }
2. shouldAsk
Determines if user should be asked based on confidence level and policy.
/**
* Determine if user should be asked for confirmation
* @param {number} confidence - Confidence score (0-100)
* @param {Object} options - Additional options
* @param {boolean} options.alwaysAsk - Force asking regardless of confidence
* @param {boolean} options.neverAsk - Never ask (for certain operations)
* @param {number} options.threshold - Custom confidence threshold (default: 80)
* @returns {Object} Decision result
*/
function shouldAsk(confidence, options = {}) {
// Override: Always ask (for safety-critical operations)
if (options.alwaysAsk) {
return {
shouldAsk: true,
reason: 'Safety-critical operation requires confirmation',
displayMode: 'CONFIRM_REQUIRED'
};
}
// Override: Never ask (for validation operations)
if (options.neverAsk) {
return {
shouldAsk: false,
reason: 'Validation operation, no user input needed',
displayMode: 'AUTO_PROCEED'
};
}
// Default threshold: 80%
const threshold = options.threshold || 80;
if (confidence >= 95) {
return {
shouldAsk: false,
reason: 'Certain - proceeding automatically',
displayMode: 'AUTO_PROCEED_SILENT'
};
} else if (confidence >= threshold) {
return {
shouldAsk: false,
reason: `High confidence (${confidence}%) - proceeding with display`,
displayMode: 'AUTO_PROCEED_WITH_DISPLAY'
};
} else if (confidence >= 50) {
return {
shouldAsk: true,
reason: `Medium confidence (${confidence}%) - suggesting with confirmation`,
displayMode: 'SUGGEST_AND_CONFIRM'
};
} else {
return {
shouldAsk: true,
reason: `Low confidence (${confidence}%) - asking user`,
displayMode: 'ASK_WITHOUT_SUGGESTION'
};
}
}
Usage Example:
// Check if we should ask
const decision = shouldAsk(65);
// Result: { shouldAsk: true, reason: "Medium confidence (65%) - suggesting with confirmation", displayMode: "SUGGEST_AND_CONFIRM" }
// Force asking for external writes
const externalWrite = shouldAsk(95, { alwaysAsk: true });
// Result: { shouldAsk: true, reason: "Safety-critical operation requires confirmation", displayMode: "CONFIRM_REQUIRED" }
3. askUserForClarification
Wrapper around AskUserQuestion tool with standardized formatting.
/**
* Ask user for clarification using AskUserQuestion tool
* @param {Object} questionConfig - Question configuration
* @param {string} questionConfig.question - The question to ask
* @param {string} questionConfig.header - Short header (max 12 chars)
* @param {Array} questionConfig.options - Array of options
* @param {any} questionConfig.options[].label - Option label
* @param {string} questionConfig.options[].description - Option description
* @param {any} questionConfig.options[].value - Option value (optional, defaults to label)
* @param {boolean} questionConfig.multiSelect - Allow multiple selections (default: false)
* @param {any} questionConfig.suggestion - Suggested value (for pre-selection)
* @param {number} questionConfig.confidence - Confidence in suggestion (for display)
* @returns {Promise<any>} User's selected value(s)
*/
async function askUserForClarification(questionConfig) {
const {
question,
header,
options,
multiSelect = false,
suggestion = null,
confidence = null
} = questionConfig;
// Format options with suggestion highlighting
const formattedOptions = options.map(opt => {
const isSuggestion = suggestion && (opt.value === suggestion || opt.label === suggestion);
return {
label: isSuggestion ? `${opt.label} ⭐ (suggested)` : opt.label,
description: isSuggestion && confidence
? `${opt.description} (${confidence}% confidence)`
: opt.description
};
});
// Display question context if medium/low confidence
if (confidence !== null && confidence < 80) {
console.log(`\n💡 AI Confidence: ${confidence}%`);
if (suggestion) {
console.log(` Suggested: ${suggestion}`);
}
console.log('');
}
// Ask user
const answer = await AskUserQuestion({
questions: [{
question,
header,
multiSelect,
options: formattedOptions
}]
});
// Extract answer (remove suggestion marker if present)
const rawAnswer = answer[header];
const cleanedAnswer = Array.isArray(rawAnswer)
? rawAnswer.map(a => a.replace(' ⭐ (suggested)', ''))
: rawAnswer.replace(' ⭐ (suggested)', '');
return cleanedAnswer;
}
Usage Example:
// Ask with suggestion (medium confidence)
const mode = await askUserForClarification({
question: "What would you like to do?",
header: "Mode",
options: [
{ label: "Create new", description: "Start from scratch" },
{ label: "Plan existing", description: "Add plan to existing issue" },
{ label: "Update plan", description: "Modify existing plan" }
],
suggestion: "Plan existing",
confidence: 65
});
// Ask without suggestion (low confidence)
const commitType = await askUserForClarification({
question: "Select commit type:",
header: "Type",
options: [
{ label: "feat", description: "New feature" },
{ label: "fix", description: "Bug fix" },
{ label: "docs", description: "Documentation" }
]
});
4. displayOptionsAndConfirm
Display what will happen and ask for confirmation.
/**
* Display proposed action and ask for confirmation
* @param {string} action - Action description
* @param {Object} details - Action details to display
* @param {Object} options - Display options
* @param {string} options.title - Section title (default: "Proposed Action")
* @param {string} options.emoji - Title emoji (default: "📋")
* @param {boolean} options.requireExplicitYes - Require "yes" instead of any confirmation (default: false)
* @returns {Promise<boolean>} True if confirmed, false otherwise
*/
async function displayOptionsAndConfirm(action, details, options = {}) {
const {
title = 'Proposed Action',
emoji = '📋',
requireExplicitYes = false
} = options;
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(`${emoji} ${title}`);
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
console.log(action);
console.log('');
// Display details
Object.entries(details).forEach(([key, value]) => {
if (Array.isArray(value)) {
console.log(`${key}:`);
value.forEach(item => console.log(` • ${item}`));
} else {
console.log(`${key}: ${value}`);
}
});
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
// Ask for confirmation
const answer = await askUserForClarification({
question: "Proceed with this action?",
header: "Confirm",
options: [
{ label: "Yes, proceed", description: "Execute the action shown above" },
{ label: "No, cancel", description: "Cancel and return" }
]
});
const confirmed = answer.toLowerCase().includes('yes');
if (!confirmed) {
console.log('❌ Cancelled\n');
}
return confirmed;
}
Usage Example:
// Display and confirm external write
const confirmed = await displayOptionsAndConfirm(
"Update Jira ticket TRAIN-456",
{
"Status": "In Progress → Done",
"Comment": "Completed via /ccpm:done",
"Labels": ["ccpm", "completed"]
},
{
title: "External System Write",
emoji: "🚨",
requireExplicitYes: true
}
);
if (confirmed) {
// Proceed with write
}
5. fuzzyMatch
Intelligent fuzzy matching with confidence scoring.
/**
* Fuzzy match input against options with confidence scoring
* @param {string} input - User input
* @param {Array} options - Array of valid options (strings or objects with 'name' property)
* @param {Object} config - Matching configuration
* @param {number} config.threshold - Minimum similarity threshold (0-100, default: 60)
* @param {boolean} config.caseSensitive - Case sensitive matching (default: false)
* @param {Array} config.aliases - Map of aliases to canonical values
* @returns {Object} Match result
*/
function fuzzyMatch(input, options, config = {}) {
const {
threshold = 60,
caseSensitive = false,
aliases = {}
} = config;
// Normalize input
const normalizedInput = caseSensitive ? input : input.toLowerCase().trim();
// Check aliases first
if (aliases[normalizedInput]) {
return {
match: aliases[normalizedInput],
confidence: 100,
exactMatch: true,
reason: 'Alias match'
};
}
// Extract option strings
const optionStrings = options.map(opt =>
typeof opt === 'string' ? opt : opt.name || opt.label || String(opt)
);
// Normalize options
const normalizedOptions = optionStrings.map(opt =>
caseSensitive ? opt : opt.toLowerCase().trim()
);
// Strategy 1: Exact match
const exactIndex = normalizedOptions.findIndex(opt => opt === normalizedInput);
if (exactIndex !== -1) {
return {
match: optionStrings[exactIndex],
confidence: 100,
exactMatch: true,
reason: 'Exact match'
};
}
// Strategy 2: Starts with
const startsWithIndex = normalizedOptions.findIndex(opt => opt.startsWith(normalizedInput));
if (startsWithIndex !== -1) {
return {
match: optionStrings[startsWithIndex],
confidence: 85,
exactMatch: false,
reason: 'Starts with match'
};
}
// Strategy 3: Contains
const containsIndex = normalizedOptions.findIndex(opt => opt.includes(normalizedInput));
if (containsIndex !== -1) {
return {
match: optionStrings[containsIndex],
confidence: 70,
exactMatch: false,
reason: 'Contains match'
};
}
// Strategy 4: Levenshtein distance (simplified)
const distances = normalizedOptions.map(opt => ({
option: opt,
distance: levenshteinDistance(normalizedInput, opt)
}));
distances.sort((a, b) => a.distance - b.distance);
const closest = distances[0];
// Calculate similarity percentage
const maxLen = Math.max(normalizedInput.length, closest.option.length);
const similarity = ((maxLen - closest.distance) / maxLen) * 100;
if (similarity >= threshold) {
const matchIndex = normalizedOptions.indexOf(closest.option);
return {
match: optionStrings[matchIndex],
confidence: Math.round(similarity),
exactMatch: false,
reason: `Fuzzy match (${closest.distance} edits)`
};
}
// No match found
return {
match: null,
confidence: 0,
exactMatch: false,
reason: 'No match found',
suggestions: optionStrings.slice(0, 3)
};
}
// Simplified Levenshtein distance
function levenshteinDistance(str1, str2) {
const len1 = str1.length;
const len2 = str2.length;
const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
for (let i = 0; i <= len1; i++) matrix[i][0] = i;
for (let j = 0; j <= len2; j++) matrix[0][j] = j;
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // deletion
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j - 1] + cost // substitution
);
}
}
return matrix[len1][len2];
}
Usage Example:
// Fuzzy match state name
const result = fuzzyMatch("in prog", [
"Backlog",
"In Progress",
"In Review",
"Done"
]);
// Result: { match: "In Progress", confidence: 70, exactMatch: false, reason: "Contains match" }
// Fuzzy match with aliases
const typeResult = fuzzyMatch("bugfix", ["feat", "fix", "docs"], {
aliases: {
"bugfix": "fix",
"feature": "feat",
"documentation": "docs"
}
});
// Result: { match: "fix", confidence: 100, exactMatch: true, reason: "Alias match" }
Validation Functions
6. validateTransition
Validate workflow state transitions.
/**
* Validate if state transition is allowed
* @param {string} fromState - Current state
* @param {string} toState - Target state
* @param {Object} stateMachine - State machine definition
* @returns {Object} Validation result
*/
function validateTransition(fromState, toState, stateMachine) {
const currentStateConfig = stateMachine[fromState];
if (!currentStateConfig) {
return {
valid: false,
confidence: 0,
error: `Unknown state: ${fromState}`,
suggestions: Object.keys(stateMachine)
};
}
const allowedNextStates = currentStateConfig.next_states || [];
if (!allowedNextStates.includes(toState)) {
return {
valid: false,
confidence: 0,
error: `Cannot transition from ${fromState} to ${toState}`,
allowedStates: allowedNextStates,
suggestions: allowedNextStates.map(s => `Try: ${s}`)
};
}
return {
valid: true,
confidence: currentStateConfig.confidence_to_transition || 90
};
}
Pattern Matching Functions
7. Common Patterns
// Issue ID pattern (PROJECT-NUMBER)
const ISSUE_ID_PATTERN = /^[A-Z]+-\d+$/;
function isIssueId(input) {
return ISSUE_ID_PATTERN.test(input);
}
function detectIssueIdConfidence(input) {
if (ISSUE_ID_PATTERN.test(input)) {
return { confidence: 95, match: true };
}
// Partial match (e.g., "PSN" or "123")
if (/^[A-Z]+$/.test(input) || /^\d+$/.test(input)) {
return { confidence: 30, match: false, suggestion: 'Provide full issue ID (e.g., PSN-29)' };
}
return { confidence: 0, match: false };
}
// Quoted string pattern (for titles)
const QUOTED_STRING_PATTERN = /^["'].*["']$/;
function isQuotedString(input) {
return QUOTED_STRING_PATTERN.test(input);
}
// Detect change type from update text
function detectChangeType(text) {
const lower = text.toLowerCase();
const patterns = {
scope_change: /(add|also|include|plus|additionally|extra)/i,
approach_change: /(instead|different|change|use.*not|replace.*with)/i,
simplification: /(remove|don't need|skip|simpler|drop|omit)/i,
blocker: /(blocked|can't|cannot|doesn't work|issue|problem|error)/i,
clarification: /.*/ // Default
};
for (const [type, pattern] of Object.entries(patterns)) {
if (pattern.test(lower) && type !== 'clarification') {
return {
type,
confidence: 75,
keywords: lower.match(pattern)
};
}
}
return {
type: 'clarification',
confidence: 50,
keywords: []
};
}
Display Helpers
8. Confidence Display
/**
* Display confidence level with appropriate emoji and color
* @param {number} confidence - Confidence score (0-100)
* @param {string} message - Message to display
*/
function displayWithConfidence(confidence, message) {
let emoji, prefix;
if (confidence >= 95) {
emoji = '✅';
prefix = 'CERTAIN';
} else if (confidence >= 80) {
emoji = '✅';
prefix = `HIGH (${confidence}%)`;
} else if (confidence >= 50) {
emoji = '⚠️';
prefix = `MEDIUM (${confidence}%)`;
} else {
emoji = '❓';
prefix = `LOW (${confidence}%)`;
}
console.log(`${emoji} ${prefix}: ${message}`);
}
/**
* Display decision summary
* @param {Object} decision - Decision result
*/
function displayDecision(decision) {
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('🎯 Decision Summary');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
displayWithConfidence(decision.confidence, decision.suggestion || decision.action);
if (decision.reasoning) {
console.log(`\n📊 Reasoning: ${decision.reasoning}`);
}
if (decision.shouldAsk) {
console.log(`\n👤 User input required`);
} else {
console.log(`\n🤖 Proceeding automatically`);
}
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
}
Error Handling
9. Structured Error Messages
/**
* Create structured error with suggestions
* @param {string} message - Error message
* @param {Object} details - Error details
* @param {Array} details.suggestions - Actionable suggestions
* @param {Array} details.availableOptions - Available options
* @returns {Error} Enhanced error object
*/
function createStructuredError(message, details = {}) {
const error = new Error(message);
error.details = details;
// Format error message with suggestions
let fullMessage = message;
if (details.availableOptions && details.availableOptions.length > 0) {
fullMessage += '\n\nAvailable options:';
details.availableOptions.forEach(opt => {
fullMessage += `\n • ${opt}`;
});
}
if (details.suggestions && details.suggestions.length > 0) {
fullMessage += '\n\nSuggestions:';
details.suggestions.forEach(suggestion => {
fullMessage += `\n • ${suggestion}`;
});
}
error.message = fullMessage;
return error;
}
Integration Example
Complete example showing all helpers working together:
// Example: Command routing with Always-Ask Policy
async function executeSmartPlan(args) {
const arg1 = args[0];
const arg2 = args[1];
// Step 1: Detect mode with confidence
const issueIdCheck = detectIssueIdConfidence(arg1);
let modeDecision;
if (issueIdCheck.match) {
// High confidence: Issue ID pattern
if (arg2) {
modeDecision = {
mode: 'UPDATE',
confidence: 95,
reasoning: 'Issue ID with update text provided'
};
} else {
modeDecision = {
mode: 'PLAN',
confidence: 95,
reasoning: 'Issue ID without update text'
};
}
} else if (isQuotedString(arg1)) {
// High confidence: Quoted title
modeDecision = {
mode: 'CREATE',
confidence: 90,
reasoning: 'Quoted string indicates new task title'
};
} else {
// Low confidence: Ambiguous
modeDecision = {
mode: null,
confidence: 30,
reasoning: 'Input format is ambiguous'
};
}
// Step 2: Check if we should ask
const askDecision = shouldAsk(modeDecision.confidence);
// Step 3: Ask user if needed
if (askDecision.shouldAsk) {
displayDecision({
...modeDecision,
shouldAsk: true,
action: `Detected input: "${arg1}"`
});
const mode = await askUserForClarification({
question: "What would you like to do?",
header: "Mode",
options: [
{ label: "Create new", description: "Create new task with this title" },
{ label: "Plan existing", description: "Plan existing issue" },
{ label: "Update plan", description: "Update existing plan" }
],
suggestion: modeDecision.mode,
confidence: modeDecision.confidence
});
modeDecision.mode = mode.replace(' new', '').replace(' existing', '').replace(' plan', '');
} else {
// High confidence: Display and proceed
displayWithConfidence(modeDecision.confidence, `Mode: ${modeDecision.mode}`);
}
// Step 4: Execute mode
switch (modeDecision.mode.toUpperCase()) {
case 'CREATE':
await executeCreate(arg1, arg2);
break;
case 'PLAN':
await executePlan(arg1);
break;
case 'UPDATE':
await executeUpdate(arg1, arg2);
break;
}
}
Best Practices
- Always calculate confidence - Don't guess, use signals
- Display confidence scores - Be transparent with users
- Provide reasoning - Explain why confidence is at a level
- Ask with suggestions - Pre-select high confidence options
- Validate early - Catch errors before execution
- Use fuzzy matching - Be forgiving with user input
- Log decisions - Track accuracy over time
- Fail gracefully - Provide actionable error messages
Testing Helpers
// Test confidence calculation
const testConfidence = calculateConfidence({
input: "PSN-29",
signals: {
patternMatch: 100,
contextMatch: 90,
historicalSuccess: 95
}
});
console.log('Confidence test:', testConfidence);
// Test fuzzy matching
const testMatch = fuzzyMatch("in prog", ["Backlog", "In Progress", "Done"]);
console.log('Fuzzy match test:', testMatch);
// Test should ask
const testAsk = shouldAsk(65);
console.log('Should ask test:', testAsk);
Performance Characteristics
| Operation | Avg Time | Notes |
|---|---|---|
| calculateConfidence | <1ms | Synchronous calculation |
| shouldAsk | <1ms | Simple threshold check |
| askUserForClarification | Varies | Waits for user input |
| fuzzyMatch | 1-5ms | Depends on options count |
| displayOptionsAndConfirm | Varies | Waits for user input |
Migration Guide
Old Pattern (no confidence tracking):
if (ISSUE_ID_PATTERN.test(arg1)) {
mode = 'PLAN';
} else {
mode = 'CREATE';
}
New Pattern (with Always-Ask Policy):
const issueIdCheck = detectIssueIdConfidence(arg1);
const decision = shouldAsk(issueIdCheck.confidence);
if (decision.shouldAsk) {
mode = await askUserForClarification({ ... });
} else {
displayWithConfidence(issueIdCheck.confidence, `Mode: ${mode}`);
}