devforgeai 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CLAUDE.md +120 -0
  2. package/package.json +9 -1
  3. package/src/CLAUDE.md +699 -0
  4. package/src/claude/scripts/README.md +396 -0
  5. package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
  6. package/src/claude/scripts/check-hooks-fast.sh +70 -0
  7. package/src/claude/scripts/devforgeai-validate +6 -0
  8. package/src/claude/scripts/devforgeai_cli/README.md +531 -0
  9. package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
  10. package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
  11. package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
  12. package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
  13. package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
  14. package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
  15. package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
  16. package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
  17. package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
  18. package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
  19. package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
  20. package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
  21. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
  22. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
  23. package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
  24. package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
  25. package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
  26. package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
  27. package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
  28. package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
  29. package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
  30. package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
  31. package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
  32. package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
  33. package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
  34. package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
  35. package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
  36. package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
  37. package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
  38. package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
  39. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
  40. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
  41. package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
  42. package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
  43. package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
  44. package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
  45. package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
  46. package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
  47. package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
  48. package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
  49. package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
  50. package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
  51. package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
  52. package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
  53. package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
  54. package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
  55. package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
  56. package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
  57. package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
  58. package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
  59. package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
  60. package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
  61. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
  62. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
  63. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
  64. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
  65. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
  66. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
  67. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
  68. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
  69. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
  70. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
  71. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
  72. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
  73. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
  74. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
  75. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
  76. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
  77. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
  78. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
  79. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
  80. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
  81. package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
  82. package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
  83. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
  84. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
  85. package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
  86. package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
  87. package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
  88. package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
  89. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
  90. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
  91. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
  92. package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
  93. package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
  94. package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
  95. package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
  96. package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
  97. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
  98. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
  99. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
  100. package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
  101. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
  102. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
  103. package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
  104. package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
  105. package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
  106. package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
  107. package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
  108. package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
  109. package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
  110. package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
  111. package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
  112. package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
  113. package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
  114. package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
  115. package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
  116. package/src/claude/scripts/install_hooks.sh +186 -0
  117. package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
  118. package/src/claude/scripts/migrate-ac-headers.sh +122 -0
  119. package/src/claude/scripts/plan_file_kb.sh +704 -0
  120. package/src/claude/scripts/requirements.txt +8 -0
  121. package/src/claude/scripts/session_catalog.sh +543 -0
  122. package/src/claude/scripts/setup.py +55 -0
  123. package/src/claude/scripts/start-devforgeai.sh +16 -0
  124. package/src/claude/scripts/statusline.sh +27 -0
  125. package/src/claude/scripts/validate_deferrals.py +344 -0
  126. package/src/claude/skills/devforgeai-qa/SKILL.md +1 -1
  127. package/src/claude/skills/researching-market/SKILL.md +2 -1
  128. package/src/cli/lib/copier.js +13 -1
  129. package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
  130. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
  131. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
  132. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
  133. package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
  134. package/src/claude/skills/devforgeai-story-creation/scripts/tests/__pycache__/measure_accuracy.cpython-312.pyc +0 -0
@@ -0,0 +1,177 @@
1
+ """
2
+ Skip tracking system for feedback collection.
3
+
4
+ This module provides atomic operations for tracking consecutive skips,
5
+ with thread-safe operations and resetting on positive feedback.
6
+ """
7
+
8
+ import os
9
+ import threading
10
+ from pathlib import Path
11
+ from typing import Optional, Dict
12
+ from datetime import datetime
13
+
14
+
15
+ class SkipTracker:
16
+ """Tracks consecutive skips for feedback collection.
17
+
18
+ Provides thread-safe atomic operations for incrementing skip counters,
19
+ checking limits, and resetting based on positive feedback.
20
+ """
21
+
22
+ # Default log path for skip tracking
23
+ DEFAULT_SKIP_LOG_PATH = Path("devforgeai/logs/feedback-skips.log")
24
+ # Default rating threshold for "positive" feedback
25
+ DEFAULT_RATING_THRESHOLD = 4
26
+
27
+ def __init__(self, skip_log_path: Optional[Path] = None):
28
+ """Initialize the skip tracker.
29
+
30
+ Args:
31
+ skip_log_path: Path to skip tracking log file.
32
+ Defaults to devforgeai/logs/feedback-skips.log
33
+ """
34
+ if skip_log_path is None:
35
+ skip_log_path = self.DEFAULT_SKIP_LOG_PATH
36
+
37
+ self.skip_log_path = skip_log_path
38
+ self._skip_counters: Dict[str, int] = {}
39
+ self._lock = threading.Lock()
40
+ self._load_existing_counters()
41
+
42
+ def _load_existing_counters(self) -> None:
43
+ """Load skip counters from log file if it exists."""
44
+ if self.skip_log_path.exists():
45
+ try:
46
+ with open(self.skip_log_path, 'r') as f:
47
+ for line in f:
48
+ line = line.strip()
49
+ if not line or line.startswith("#"):
50
+ continue
51
+ parts = line.split(":")
52
+ if len(parts) >= 3:
53
+ # Format: timestamp:operation:count
54
+ operation = parts[1].strip()
55
+ try:
56
+ count = int(parts[2].split(",")[0].strip())
57
+ self._skip_counters[operation] = count
58
+ except (ValueError, IndexError):
59
+ pass
60
+ except (IOError, OSError):
61
+ # File read error - continue with empty counters
62
+ pass
63
+
64
+ def _ensure_log_directory(self) -> None:
65
+ """Ensure the logs directory exists."""
66
+ self.skip_log_path.parent.mkdir(parents=True, exist_ok=True)
67
+
68
+ def _log_skip_operation(self, operation: str, count: int, action: str) -> None:
69
+ """Log a skip operation to the tracking file.
70
+
71
+ Args:
72
+ operation: Name of the operation.
73
+ count: Current skip count.
74
+ action: Action performed (skip, reset, block).
75
+ """
76
+ self._ensure_log_directory()
77
+ timestamp = datetime.now().isoformat()
78
+ try:
79
+ with open(self.skip_log_path, 'a') as f:
80
+ f.write(f"{timestamp}: {operation}: {count}, action={action}\n")
81
+ except (IOError, OSError):
82
+ # Silently fail if log write fails
83
+ pass
84
+
85
+ def increment_skip(self, operation: str) -> int:
86
+ """Increment skip counter for an operation (thread-safe).
87
+
88
+ Args:
89
+ operation: Name of the operation that was skipped.
90
+
91
+ Returns:
92
+ Updated skip count for the operation.
93
+ """
94
+ with self._lock:
95
+ current = self._skip_counters.get(operation, 0)
96
+ current += 1
97
+ self._skip_counters[operation] = current
98
+ self._log_skip_operation(operation, current, "skip")
99
+ return current
100
+
101
+ def get_skip_count(self, operation: str) -> int:
102
+ """Get current skip count for an operation (thread-safe).
103
+
104
+ Args:
105
+ operation: Name of the operation.
106
+
107
+ Returns:
108
+ Current skip count (0 if never skipped).
109
+ """
110
+ with self._lock:
111
+ return self._skip_counters.get(operation, 0)
112
+
113
+ def reset_skip_counter(self, operation: str) -> None:
114
+ """Reset skip counter for an operation (thread-safe).
115
+
116
+ Args:
117
+ operation: Name of the operation.
118
+ """
119
+ with self._lock:
120
+ if operation in self._skip_counters:
121
+ self._skip_counters[operation] = 0
122
+ self._log_skip_operation(operation, 0, "reset")
123
+
124
+ def check_skip_limit(self, operation: str, max_consecutive_skips: int) -> bool:
125
+ """Check if skip limit has been reached (thread-safe).
126
+
127
+ Args:
128
+ operation: Name of the operation.
129
+ max_consecutive_skips: Maximum allowed consecutive skips.
130
+ 0 = unlimited.
131
+
132
+ Returns:
133
+ True if limit reached (should block), False otherwise.
134
+ Returns False if max_consecutive_skips is 0 (unlimited).
135
+ """
136
+ if max_consecutive_skips == 0:
137
+ # Unlimited skips
138
+ return False
139
+
140
+ with self._lock:
141
+ count = self._skip_counters.get(operation, 0)
142
+ if count >= max_consecutive_skips:
143
+ self._log_skip_operation(operation, count, "block")
144
+ return True
145
+ return False
146
+
147
+ def reset_on_positive(self, operation: str, rating: int, rating_threshold: Optional[int] = None) -> None:
148
+ """Reset skip counter if positive feedback received (thread-safe).
149
+
150
+ Args:
151
+ operation: Name of the operation.
152
+ rating: User's feedback rating/score.
153
+ rating_threshold: Rating value above which is considered positive.
154
+ Defaults to DEFAULT_RATING_THRESHOLD.
155
+ """
156
+ if rating_threshold is None:
157
+ rating_threshold = self.DEFAULT_RATING_THRESHOLD
158
+
159
+ if rating >= rating_threshold:
160
+ self.reset_skip_counter(operation)
161
+
162
+ def get_all_counters(self) -> Dict[str, int]:
163
+ """Get copy of all skip counters (thread-safe).
164
+
165
+ Returns:
166
+ Dictionary of operation -> skip count.
167
+ """
168
+ with self._lock:
169
+ return self._skip_counters.copy()
170
+
171
+ def clear_all_counters(self) -> None:
172
+ """Clear all skip counters (thread-safe).
173
+
174
+ Used for testing and reset scenarios.
175
+ """
176
+ with self._lock:
177
+ self._skip_counters.clear()
@@ -0,0 +1,221 @@
1
+ """
2
+ Skip tracking functionality.
3
+
4
+ Tracks when users skip feedback and triggers suggestions after 3+ consecutive skips.
5
+ """
6
+
7
+ import yaml
8
+ import os
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Optional, Callable
12
+
13
+
14
+ # Configure logging
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ # ============================================================================
19
+ # CONFIG FILE I/O (Private)
20
+ # ============================================================================
21
+
22
+ def _get_config_file(config_dir: Optional[Path] = None) -> Path:
23
+ """
24
+ Get feedback config file path.
25
+
26
+ Args:
27
+ config_dir: Config directory (default: devforgeai/config)
28
+
29
+ Returns:
30
+ Path to feedback-preferences.yaml (per STORY-009 specification)
31
+ """
32
+ if config_dir is None:
33
+ config_dir = Path.cwd() / 'devforgeai' / 'config'
34
+
35
+ config_dir.mkdir(parents=True, exist_ok=True)
36
+ return config_dir / 'feedback-preferences.yaml'
37
+
38
+
39
+ def _load_config(config_file: Path) -> dict:
40
+ """
41
+ Load config from YAML file.
42
+
43
+ Args:
44
+ config_file: Path to config file
45
+
46
+ Returns:
47
+ Config dictionary (defaults to empty skip_counts if missing)
48
+ """
49
+ if not config_file.exists():
50
+ return {'skip_counts': {}}
51
+
52
+ with open(config_file, 'r') as f:
53
+ config = yaml.safe_load(f)
54
+
55
+ return config or {'skip_counts': {}}
56
+
57
+
58
+ def _save_config(config: dict, config_file: Path) -> None:
59
+ """
60
+ Save config to YAML file with proper permissions.
61
+
62
+ Sets file permissions to mode 600 (user-readable/writable only).
63
+
64
+ Args:
65
+ config: Config dictionary
66
+ config_file: Path to config file
67
+ """
68
+ with open(config_file, 'w') as f:
69
+ yaml.safe_dump(config, f, default_flow_style=False)
70
+
71
+ # Set file permissions to 600 (user read/write only)
72
+ try:
73
+ os.chmod(config_file, 0o600)
74
+ logger.debug(f"Set config file permissions to 600: {config_file}")
75
+ except OSError as e:
76
+ logger.warning(f"Could not set config file permissions: {e}")
77
+
78
+
79
+ def validate_config_permissions(config_file: Path) -> bool:
80
+ """
81
+ Validate that config file has appropriate permissions (mode 600).
82
+
83
+ Args:
84
+ config_file: Path to config file
85
+
86
+ Returns:
87
+ True if permissions are 600, False otherwise
88
+ """
89
+ if not config_file.exists():
90
+ return True # File doesn't exist yet
91
+
92
+ try:
93
+ file_stat = config_file.stat()
94
+ # Extract permission bits
95
+ permissions = file_stat.st_mode & 0o777
96
+ is_valid = permissions == 0o600
97
+
98
+ if not is_valid:
99
+ logger.warning(
100
+ f"Config file has insecure permissions: "
101
+ f"{oct(permissions)} (should be 0o600)"
102
+ )
103
+
104
+ return is_valid
105
+ except OSError as e:
106
+ logger.warning(f"Could not validate config file permissions: {e}")
107
+ return False
108
+
109
+
110
+ def _apply_config_modification(config_file: Path, modifier_fn: Callable[[dict], dict]) -> None:
111
+ """
112
+ Apply modification to config atomically (DRY helper).
113
+
114
+ Encapsulates read-modify-write pattern to reduce duplication.
115
+
116
+ Args:
117
+ config_file: Path to config file
118
+ modifier_fn: Function that receives config dict and returns modified dict
119
+ """
120
+ config = _load_config(config_file)
121
+ modified_config = modifier_fn(config)
122
+ _save_config(modified_config, config_file)
123
+
124
+
125
+ # ============================================================================
126
+ # SKIP COUNTER OPERATIONS (Public)
127
+ # ============================================================================
128
+
129
+ def increment_skip(operation_type: str, config_dir: Optional[Path] = None) -> int:
130
+ """
131
+ Increment skip count for operation type.
132
+
133
+ Args:
134
+ operation_type: Operation type (e.g., 'skill_invocation', 'subagent_invocation')
135
+ config_dir: Config directory
136
+
137
+ Returns:
138
+ New skip count
139
+ """
140
+ config_file = _get_config_file(config_dir)
141
+
142
+ def modify_config(config):
143
+ if 'skip_counts' not in config:
144
+ config['skip_counts'] = {}
145
+
146
+ current_count = config['skip_counts'].get(operation_type, 0)
147
+ new_count = current_count + 1
148
+ config['skip_counts'][operation_type] = new_count
149
+ return config
150
+
151
+ _apply_config_modification(config_file, modify_config)
152
+
153
+ # Reload to get updated count
154
+ config = _load_config(config_file)
155
+ return config['skip_counts'][operation_type]
156
+
157
+
158
+ def get_skip_count(operation_type: str, config_dir: Optional[Path] = None) -> int:
159
+ """
160
+ Get current skip count for operation type.
161
+
162
+ Args:
163
+ operation_type: Operation type (e.g., 'skill_invocation', 'subagent_invocation')
164
+ config_dir: Config directory
165
+
166
+ Returns:
167
+ Current skip count
168
+ """
169
+ config_file = _get_config_file(config_dir)
170
+ config = _load_config(config_file)
171
+ return config.get('skip_counts', {}).get(operation_type, 0)
172
+
173
+
174
+ def reset_skip_count(operation_type: str, config_dir: Optional[Path] = None) -> None:
175
+ """
176
+ Reset skip count for operation type to 0.
177
+
178
+ Args:
179
+ operation_type: Operation type (e.g., 'skill_invocation', 'subagent_invocation')
180
+ config_dir: Config directory
181
+ """
182
+ config_file = _get_config_file(config_dir)
183
+
184
+ def modify_config(config):
185
+ if 'skip_counts' not in config:
186
+ config['skip_counts'] = {}
187
+
188
+ config['skip_counts'][operation_type] = 0
189
+ return config
190
+
191
+ _apply_config_modification(config_file, modify_config)
192
+
193
+
194
+ def check_skip_threshold(operation_type: str, threshold: int = 3, config_dir: Optional[Path] = None) -> bool:
195
+ """
196
+ Check if operation type has reached skip threshold.
197
+
198
+ Args:
199
+ operation_type: Operation type (e.g., 'skill_invocation', 'subagent_invocation')
200
+ threshold: Skip threshold (default: 3)
201
+ config_dir: Config directory
202
+
203
+ Returns:
204
+ True if threshold reached, False otherwise
205
+ """
206
+ count = get_skip_count(operation_type, config_dir)
207
+ return count >= threshold
208
+
209
+
210
+ # ============================================================================
211
+ # PUBLIC API (Export for external use)
212
+ # ============================================================================
213
+
214
+ __all__ = [
215
+ 'increment_skip',
216
+ 'get_skip_count',
217
+ 'reset_skip_count',
218
+ 'check_skip_threshold',
219
+ 'validate_config_permissions',
220
+ ]
221
+