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.
- package/CLAUDE.md +120 -0
- package/package.json +9 -1
- package/src/CLAUDE.md +699 -0
- package/src/claude/scripts/README.md +396 -0
- package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
- package/src/claude/scripts/check-hooks-fast.sh +70 -0
- package/src/claude/scripts/devforgeai-validate +6 -0
- package/src/claude/scripts/devforgeai_cli/README.md +531 -0
- package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
- package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
- package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
- package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
- package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
- package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
- package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
- package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
- package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
- package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
- package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
- package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
- package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
- package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
- package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
- package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
- package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
- package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
- package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
- package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
- package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
- package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
- package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
- package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
- package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
- package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
- package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
- package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
- package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
- package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
- package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
- package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
- package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
- package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
- package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
- package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
- package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
- package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
- package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
- package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
- package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
- package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
- package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
- package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
- package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
- package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
- package/src/claude/scripts/install_hooks.sh +186 -0
- package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
- package/src/claude/scripts/migrate-ac-headers.sh +122 -0
- package/src/claude/scripts/plan_file_kb.sh +704 -0
- package/src/claude/scripts/requirements.txt +8 -0
- package/src/claude/scripts/session_catalog.sh +543 -0
- package/src/claude/scripts/setup.py +55 -0
- package/src/claude/scripts/start-devforgeai.sh +16 -0
- package/src/claude/scripts/statusline.sh +27 -0
- package/src/claude/scripts/validate_deferrals.py +344 -0
- package/src/claude/skills/devforgeai-qa/SKILL.md +1 -1
- package/src/claude/skills/researching-market/SKILL.md +2 -1
- package/src/cli/lib/copier.js +13 -1
- package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
- 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
|
+
|