Power-Steering Merge Preference Awareness - Technical Reference¶
Technical documentation for the Power-Steering merge preference awareness feature.
Overview¶
Power-Steering respects the USER_PREFERENCES.md setting "NEVER Merge PRs Without Permission". When active, it treats "PR ready + CI passing + awaiting user approval" as a valid completion state, rather than requiring PR merge.
Key Components:
- Detection:
_user_prefers_no_auto_merge()method - Validation:
_check_ci_status_no_auto_merge()method - Integration:
_check_ci_status()method modification - Location:
.claude/tools/amplihack/hooks/power_steering_checker.py
Architecture¶
Component Overview¶
[Power-Steering Checker]
|
v
[_check_ci_status()] -----> reads USER_PREFERENCES.md
|
v
[_user_prefers_no_auto_merge()] -----> regex detection
|
yes / \ no
/ \
v v
[_check_ci_status_no_auto_merge()] [standard CI check]
| |
v v
"PR ready + CI passing" "PR must be merged"
Design Principles¶
- Fail-Open: Errors default to standard behavior, never falsely satisfy
- Non-Invasive: No changes to considerations.yaml required
- User-Centric: Respects explicit user preferences
- Robust: Handles missing files, regex errors, PR status failures
API Reference¶
Detection Method¶
def _user_prefers_no_auto_merge(self) -> bool:
"""
Detect if user has set preference to never auto-merge PRs.
Reads USER_PREFERENCES.md and searches for the pattern:
"NEVER ... Merge ... PR ... Without ... Permission"
Returns:
bool: True if preference detected, False otherwise
Behavior:
- Case-insensitive regex matching
- Handles multiline preferences
- Fail-open: returns False on any error
- File path: .claude/context/USER_PREFERENCES.md
Examples:
Detected patterns:
- "NEVER Merge PRs Without Permission"
- "NEVER merge pull requests without explicit permission"
- "Never Merge PRs or Commit Without Explicit Permission"
Not detected:
- "Never commit without permission" (missing "merge" + "PR")
- "NEVER auto-merge" (missing "without permission")
"""
Implementation Notes:
- Regex pattern:
r'NEVER.*(?:Merge|merge).*(?:PR|Pull Request).*(?:Without|without).*(?:Permission|permission)' - Uses
re.IGNORECASEandre.DOTALLflags - Returns
FalseonFileNotFoundError,IOError,re.error - Logs errors at WARNING level (does not raise)
Validation Method¶
def _check_ci_status_no_auto_merge(self) -> CheckResult:
"""
Validate CI status WITHOUT requiring PR merge.
Checks:
1. PR exists and is open
2. CI checks are passing
3. PR is ready for review (not draft)
Returns:
CheckResult: Satisfied if PR ready + CI passing, else Unsatisfied
Success Criteria:
- PR found via gh CLI
- PR state is "OPEN"
- CI status is "SUCCESS" or all checks passing
- Not in draft state
Failure Modes:
- No PR found → Unsatisfied ("No PR found")
- CI checks failing → Unsatisfied ("CI checks failing")
- gh CLI error → Unsatisfied (error message)
Example Success Output:
CheckResult(
satisfied=True,
details="PR #123 ready (CI passing, awaiting user approval)",
evidence=["gh pr view output"]
)
"""
Implementation Notes:
- Uses
gh pr viewto fetch PR status - Parses JSON response for
statusCheckRollupandstate - Does NOT check
mergeableormergedstatus - Collects evidence: PR number, CI status, review state
Integration Method¶
def _check_ci_status(self) -> CheckResult:
"""
Check CI status with preference awareness.
Flow:
1. Call _user_prefers_no_auto_merge()
2. If True: call _check_ci_status_no_auto_merge()
3. If False: use standard CI check logic
Returns:
CheckResult: Result from appropriate checker method
Behavior:
- Transparent to caller
- No changes to CheckResult format
- Maintains backward compatibility
"""
Data Structures¶
CheckResult¶
@dataclass
class CheckResult:
"""Result of a Power-Steering consideration check."""
satisfied: bool
"""Whether the check passed."""
details: str
"""Human-readable explanation of result."""
evidence: List[str] = field(default_factory=list)
"""Supporting evidence (command outputs, file contents)."""
error: Optional[str] = None
"""Error message if check failed due to error."""
Usage in Preference Awareness:
# Success case
CheckResult(
satisfied=True,
details="PR #123 ready for review (CI passing, awaiting user approval)",
evidence=[
"gh pr view output",
"CI status: SUCCESS",
"Review state: APPROVED"
]
)
# Failure case
CheckResult(
satisfied=False,
details="CI checks still running",
evidence=["gh pr view output"],
error=None
)
# Error case
CheckResult(
satisfied=False,
details="Failed to check PR status",
evidence=[],
error="gh CLI not authenticated"
)
File Paths¶
USER_PREFERENCES.md¶
Location: .claude/context/USER_PREFERENCES.md
Format:
### YYYY-MM-DD HH:MM:SS
**Preference Title**
Preference description. Must include keywords:
- NEVER
- Merge (or merge)
- PR (or Pull Request)
- Without (or without)
- Permission (or permission)
**Implementation Requirements** (optional):
- Additional details
Example:
### 2026-01-23 10:00:00
**NEVER Merge PRs or Commit Directly Without Explicit Permission**
NEVER merge PRs or commit directly to main without explicit user permission.
Always create PRs and wait for approval. Only the first explicitly approved
merge applies - subsequent PRs require separate approval.
**Implementation Requirements:**
- MUST create PR and wait for user to say "merge" or "please merge"
- MUST ask for permission for EACH PR merge separately
Power-Steering Checker¶
Location: .claude/tools/amplihack/hooks/power_steering_checker.py
Class: PowerSteeringChecker
Modified Methods:
_check_ci_status()- Entry point, delegates based on preference_user_prefers_no_auto_merge()- Detection logic (NEW)_check_ci_status_no_auto_merge()- Validation logic (NEW)
Integration Points¶
Lazy Detection Design¶
Preference detection occurs during each _check_ci_status() call, not at initialization:
# In _check_ci_status()
if self._user_prefers_no_auto_merge():
return self._check_ci_status_no_auto_merge()
else:
# Standard CI check logic
return self._check_ci_status_standard()
⚙️ Design Rationale: Lazy Detection
Power-Steering reads
USER_PREFERENCES.mdduring each CI check rather than at initialization. This design provides:
- Zero Startup Overhead: No file I/O during hook initialization
- Dynamic Updates: Preference changes take effect immediately without restart
- Fail-Open Safety: If
USER_PREFERENCES.mdis temporarily unavailable, the system continues with standard behavior- Resilience: Detection errors never prevent CI validation
The file read overhead (<1ms) is negligible compared to
ghCLI network calls (100-500ms).
Key Behaviors:
- Preference state is NOT cached between checks
- Changes to
USER_PREFERENCES.mdare detected on next check - No restart required when adding/removing preference
- File read errors default to standard behavior (fail-open)
gh CLI Integration¶
Uses GitHub CLI to fetch PR status:
# Command executed
gh pr view --json state,statusCheckRollup,isDraft
# Expected JSON response
{
"state": "OPEN",
"isDraft": false,
"statusCheckRollup": [
{"state": "SUCCESS", "context": "ci/test"},
{"state": "SUCCESS", "context": "ci/lint"}
]
}
Requirements:
ghCLI installed and in PATH- Authenticated:
gh auth statussucceeds - Repository has GitHub remote configured
Fallback: If gh CLI unavailable, check falls back to standard behavior.
Error Handling¶
Error Scenarios¶
| Error Type | Trigger | Behavior | User Impact |
|---|---|---|---|
| File not found | USER_PREFERENCES.md missing | Return False from detection | Standard CI check runs |
| Read error | Permission denied, I/O error | Return False from detection | Standard CI check runs |
| Regex error | Invalid pattern (unlikely) | Return False from detection | Standard CI check runs |
| gh CLI missing | gh not in PATH | Return unsatisfied | User notified to install gh |
| gh CLI auth fail | Not authenticated | Return unsatisfied | User notified to run gh auth |
| PR not found | No PR created yet | Return unsatisfied | Expected - user hasn't created PR |
| CI checks fail | Tests failing | Return unsatisfied | Expected - user must fix tests |
Fail-Open Principle¶
Definition: When in doubt, default to safe behavior that doesn't falsely satisfy checks.
Implementation:
try:
# Attempt preference detection
if self._user_prefers_no_auto_merge():
return self._check_ci_status_no_auto_merge()
except Exception as e:
logger.warning(f"Error detecting merge preference: {e}")
# Fall through to standard behavior
# Standard CI check (requires merge)
return self._check_ci_status_standard()
Rationale:
- Errors during detection should never prevent CI validation
- Better to require merge when in doubt than skip validation
- Preserves backward compatibility
Testing¶
Unit Tests¶
Test coverage for preference awareness:
# test_power_steering_checker.py
class TestMergePreferenceAwareness:
"""Tests for USER_PREFERENCES merge preference detection."""
def test_preference_detected_standard_format(self):
"""NEVER Merge PRs Without Permission detected."""
def test_preference_detected_verbose_format(self):
"""NEVER merge pull requests without explicit user permission detected."""
def test_preference_not_detected_missing_keywords(self):
"""Preference with missing keywords not detected."""
def test_preference_detection_file_not_found(self):
"""Missing USER_PREFERENCES.md returns False."""
def test_ci_check_no_auto_merge_pr_ready(self):
"""PR ready + CI passing returns satisfied."""
def test_ci_check_no_auto_merge_ci_failing(self):
"""PR ready + CI failing returns unsatisfied."""
def test_ci_check_no_auto_merge_no_pr(self):
"""No PR created returns unsatisfied."""
def test_ci_check_standard_behavior_without_preference(self):
"""Standard behavior when preference not detected."""
Integration Tests¶
End-to-end validation:
class TestMergePreferenceIntegration:
"""Integration tests for merge preference workflow."""
def test_workflow_with_preference_stops_at_pr_ready(self):
"""Workflow completes when PR ready (not merged)."""
def test_workflow_without_preference_requires_merge(self):
"""Workflow requires merge when preference not set."""
def test_preference_change_during_session(self):
"""Preference changes reflected in subsequent checks."""
Manual Testing¶
Test Plan:
- Setup: Create test repository with USER_PREFERENCES.md
- Create PR: Push code, create PR, wait for CI
- Run Power-Steering: Verify consideration check passes
- Verify Evidence: Check logs for "awaiting user approval" message
- Remove Preference: Comment out preference, verify standard behavior
Performance Considerations¶
File I/O¶
USER_PREFERENCES.mdread during each_check_ci_status()call (lazy detection)- Typical size: <10KB
- Read overhead: <1ms
- No caching between checks (enables dynamic preference changes)
Note: Could cache preference state per session if file I/O becomes measurable overhead, but current design prioritizes dynamic updates.
Regex Performance¶
- Pattern complexity: Medium (5 groups, quantifiers)
- Typical input size: 1-100 lines
- Match overhead: <1ms
Note: Performance negligible compared to network I/O (gh CLI).
gh CLI Overhead¶
- Network latency: 100-500ms (GitHub API)
- Dominant performance factor
- Unavoidable for accurate PR status
Security Considerations¶
Input Validation¶
USER_PREFERENCES.md:
- File read with encoding='utf-8'
- No code execution risk (text file only)
- Regex matching is safe (no eval/exec)
gh CLI Output:
- JSON parsing with built-in
jsonmodule - No shell injection (uses subprocess.run with list args)
- Output sanitized before logging
Privilege Escalation¶
Risk: None. Feature reads configuration, doesn't modify state.
Validation:
- No file writes
- No git operations
- No GitHub API mutations
Information Disclosure¶
Risk: Low. Logs may contain PR numbers and status.
Mitigation:
- Logs written to
.claude/runtime/logs/(gitignored) - No secrets logged (API tokens, passwords)
- Evidence collection uses sanitized output
Migration Guide¶
From Standard Behavior¶
Before (standard Power-Steering):
# considerations.yaml
- id: ci_status
question: Are CI checks passing and PR merged?
checker: _check_ci_status
After (with preference awareness):
# No changes to considerations.yaml required
- id: ci_status
question: Are CI checks passing and PR merged?
checker: _check_ci_status # Now preference-aware
Migration Steps:
- Update Power-Steering code (already done)
- Add preference to
USER_PREFERENCES.md(user action) - No restart needed - changes detected on next CI check
- Verify behavior with test PR
Note: If creating USER_PREFERENCES.md for the first time, restart Claude to ensure the file is accessible. Subsequent changes take effect immediately.
Troubleshooting¶
Debug Logging¶
Enable detailed logging:
# In power_steering_checker.py
logger.setLevel(logging.DEBUG)
# Logs output
DEBUG: Reading USER_PREFERENCES.md
DEBUG: Preference pattern: NEVER.*Merge.*PR.*Without.*Permission
DEBUG: Preference detected: True
DEBUG: Using no-auto-merge CI check
DEBUG: Executing: gh pr view --json state,statusCheckRollup,isDraft
DEBUG: PR #123 state: OPEN, CI: SUCCESS
INFO: CI check satisfied (PR ready, awaiting user approval)
Common Issues¶
Issue: Preference detected but CI check still fails
Diagnosis:
# Check PR status manually
gh pr view --json state,statusCheckRollup,isDraft
# Expected output
{
"state": "OPEN",
"statusCheckRollup": [{"state": "SUCCESS"}]
}
Solution: Ensure CI checks actually passing, PR not in draft state.
Issue: Preference not detected despite correct format
Diagnosis:
# Check file exists and has correct content
cat .claude/context/USER_PREFERENCES.md | grep -i "never.*merge.*without.*permission"
Solution:
- Verify file location at
.claude/context/USER_PREFERENCES.md - If file was just created, restart Claude session (first-time only)
- If file already existed, changes take effect immediately on next check
Related Documentation¶
- How-To: Configure Merge Preferences - User guide
- Power-Steering Overview - Feature overview
- Power-Steering Configuration - General configuration
- USER_PREFERENCES.md Reference - Complete preferences documentation
Changelog¶
v0.10.0 (Planned)¶
Added:
_user_prefers_no_auto_merge()method for preference detection_check_ci_status_no_auto_merge()method for no-merge validation- Preference-aware logic in
_check_ci_status()
Changed:
_check_ci_status()now respects USER_PREFERENCES.md merge setting
Technical Details:
- No breaking changes
- Backward compatible with existing workflows
- Fail-open design for robustness
Future Enhancements¶
Planned Improvements¶
- Caching: Cache preference state per session to reduce file I/O
- Configuration: Add explicit toggle in considerations.yaml for preference awareness
- Metrics: Track preference usage and CI check success rates
- Documentation: Auto-detect preference format variations
Research Directions¶
- Granular Preferences: Per-PR merge preferences (e.g., "auto-merge bug fixes, manual for features")
- Time-Based: Preference inheritance from parent branch or project defaults
- Team Policies: Organization-wide merge preferences
Questions? Open an issue on GitHub with the power-steering label.