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,426 @@
1
+ """
2
+ Context Extraction for DevForgeAI Feedback Hooks
3
+
4
+ Extracts operation context from TodoWrite, including:
5
+ - Todos (status, content)
6
+ - Errors (messages, stack traces)
7
+ - Timing (start_time, end_time, duration)
8
+ - Secret sanitization (50+ patterns)
9
+ - Context size limiting (50KB max)
10
+
11
+ Uses regex patterns to sanitize sensitive data before skill invocation.
12
+ """
13
+
14
+ import json
15
+ import logging
16
+ import re
17
+ from datetime import datetime
18
+ from typing import Dict, List, Any, Optional
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Constants for context limiting
23
+ MAX_CONTEXT_SIZE_BYTES = 50 * 1024
24
+ MAX_TODOS_BEFORE_SUMMARY = 100
25
+ MAX_ERRORS_BEFORE_TRUNCATION = 10
26
+ TRUNCATION_MARKER = "... truncated"
27
+
28
+ # Secret patterns for sanitization (50+ patterns)
29
+ SECRET_PATTERNS = [
30
+ # API Keys
31
+ (r"(api[_-]?key\s*[:=]\s*)([sk-][a-zA-Z0-9]{20,})", r"\1***"),
32
+ (r"(apikey\s*[:=]\s*)([a-zA-Z0-9\._\-]{32,})", r"\1***"),
33
+ (r"(api[_-]?secret\s*[:=]\s*)(\S+)", r"\1***"),
34
+ (r"(sk[_-]proj\s*[:=]\s*)(\S+)", r"\1***"),
35
+ (r"(sk[_-]live\s*[:=]\s*)(\S+)", r"\1***"),
36
+
37
+ # Passwords
38
+ (r"(password\s*[:=]\s*)(\S+)", r"\1***"),
39
+ (r"(passwd\s*[:=]\s*)(\S+)", r"\1***"),
40
+ (r"(pwd\s*[:=]\s*)(\S+)", r"\1***"),
41
+ (r"(user[_-]?password\s*[:=]\s*)(\S+)", r"\1***"),
42
+ (r"(secret\s*[:=]\s*)(\S+)", r"\1***"),
43
+
44
+ # OAuth Tokens
45
+ (r"(access[_-]?token\s*[:=]\s*)([a-zA-Z0-9\._\-]{20,})", r"\1***"),
46
+ (r"(refresh[_-]?token\s*[:=]\s*)([a-zA-Z0-9\._\-]{20,})", r"\1***"),
47
+ (r"(token\s*[:=]\s*)(bearer[_\s]*[a-zA-Z0-9\._\-]+)", r"\1***"),
48
+ (r"(oauth[_-]?token\s*[:=]\s*)(\S+)", r"\1***"),
49
+ (r"(id[_-]?token\s*[:=]\s*)(\S+)", r"\1***"),
50
+
51
+ # AWS Keys and Credentials
52
+ (r"(AKIA[0-9A-Z]{16})", r"***"),
53
+ (r"(aws[_-]?access[_-]?key[_-]?id\s*[:=]\s*)(AKIA[0-9A-Z]{16})", r"\1***"),
54
+ (r"(aws[_-]?secret[_-]?access[_-]?key\s*[:=]\s*)(\S+)", r"\1***"),
55
+ (r"(aws[_-]?key\s*[:=]\s*)(\S+)", r"\1***"),
56
+ (r"(session[_-]?token\s*[:=]\s*)(\S+)", r"\1***"),
57
+
58
+ # Database Credentials
59
+ (r"(database[_-]?url\s*[:=]\s*)([a-z]+://[^:]+):([^@]+)(@.*)", r"\1\2:***\4"),
60
+ (r"(database[_-]?password\s*[:=]\s*)(\S+)", r"\1***"),
61
+ (r"(db[_-]?password\s*[:=]\s*)(\S+)", r"\1***"),
62
+ (r"(mongodb[_-]?uri\s*[:=]\s*)([a-z]+://[^:]+):([^@]+)(@.*)", r"\1\2:***\3"),
63
+ (r"(postgres[_-]?password\s*[:=]\s*)(\S+)", r"\1***"),
64
+ (r"(mysql[_-]?password\s*[:=]\s*)(\S+)", r"\1***"),
65
+
66
+ # GCP Credentials
67
+ (r"(GCP[_-]?SERVICE[_-]?ACCOUNT[_-]?KEY\s*[:=]\s*)(\{.*?\})", r"\1***"),
68
+ (r"(GOOGLE[_-]?CLOUD[_-]?API[_-]?KEY\s*[:=]\s*)(\S+)", r"\1***"),
69
+ (r"(gcp[_-]?key\s*[:=]\s*)(\S+)", r"\1***"),
70
+ (r"(service[_-]?account[_-]?key\s*[:=]\s*)(\{.*?\})", r"\1***"),
71
+
72
+ # GitHub Tokens
73
+ (r"(github[_-]?token\s*[:=]\s*)(ghp_[a-zA-Z0-9]{36})", r"\1***"),
74
+ (r"(github[_-]?pat\s*[:=]\s*)(ghp_[a-zA-Z0-9]{36})", r"\1***"),
75
+ (r"(github[_-]?key\s*[:=]\s*)(\S+)", r"\1***"),
76
+ (r"(github[_-]?secret\s*[:=]\s*)(\S+)", r"\1***"),
77
+
78
+ # SSH Keys
79
+ (r"(-----BEGIN RSA PRIVATE KEY-----)", r"[REDACTED SSH KEY]"),
80
+ (r"(-----BEGIN OPENSSH PRIVATE KEY-----)", r"[REDACTED SSH KEY]"),
81
+ (r"(ssh[_-]?key\s*[:=]\s*)(\S+)", r"\1***"),
82
+ (r"(private[_-]?key\s*[:=]\s*)(\S+)", r"\1***"),
83
+
84
+ # JWT Tokens
85
+ (r"(jwt\s*[:=]\s*)([a-zA-Z0-9\-\._]+\.[a-zA-Z0-9\-\._]+\.[a-zA-Z0-9\-\._]+)", r"\1***"),
86
+ (r"(bearer\s+)([a-zA-Z0-9\-\._]+\.[a-zA-Z0-9\-\._]+\.[a-zA-Z0-9\-\._]+)", r"\1***"),
87
+ (r"(authorization\s*[:=]\s*Bearer\s+)([a-zA-Z0-9\-\._]+)", r"\1***"),
88
+
89
+ # PII Patterns
90
+ (r"(ssn\s*[:=]\s*)(\d{3}-\d{2}-\d{4})", r"\1***"),
91
+ (r"(credit[_-]?card\s*[:=]\s*)(\d{4}[_\s]?\d{4}[_\s]?\d{4}[_\s]?\d{4})", r"\1***"),
92
+ (r"(social[_-]?security[_-]?number\s*[:=]\s*)(\S+)", r"\1***"),
93
+ (r"(phone[_-]?number\s*[:=]\s*)(\S+)", r"\1***"),
94
+ (r"(email\s*[:=]\s*)([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+)", r"\1***"),
95
+
96
+ # Other tokens
97
+ (r"(slack[_-]?token\s*[:=]\s*)(\S+)", r"\1***"),
98
+ (r"(discord[_-]?token\s*[:=]\s*)(\S+)", r"\1***"),
99
+ (r"(twilio[_-]?auth[_-]?token\s*[:=]\s*)(\S+)", r"\1***"),
100
+ (r"(auth[_-]?token\s*[:=]\s*)(\S+)", r"\1***"),
101
+ (r"(bearer[_-]?token\s*[:=]\s*)(\S+)", r"\1***"),
102
+
103
+ # Additional patterns
104
+ (r"(cert[_-]?password\s*[:=]\s*)(\S+)", r"\1***"),
105
+ (r"(certificate[_-]?password\s*[:=]\s*)(\S+)", r"\1***"),
106
+ (r"(encryption[_-]?key\s*[:=]\s*)(\S+)", r"\1***"),
107
+ (r"(private[_-]?secret\s*[:=]\s*)(\S+)", r"\1***"),
108
+ (r"(sensitive[_-]?data\s*[:=]\s*)(\S+)", r"\1***"),
109
+ ]
110
+
111
+
112
+ class ContextExtractor:
113
+ """Extracts operation context from TodoWrite and other sources."""
114
+
115
+ def __init__(self, max_size: int = MAX_CONTEXT_SIZE_BYTES):
116
+ """
117
+ Initialize ContextExtractor.
118
+
119
+ Args:
120
+ max_size: Maximum context size in bytes (default: 50KB)
121
+ """
122
+ self.max_size = max_size
123
+
124
+ def extract_operation_context(
125
+ self, operation: str, story_id: Optional[str] = None
126
+ ) -> Dict[str, Any]:
127
+ """
128
+ Extract context for an operation.
129
+
130
+ Args:
131
+ operation: Operation name (e.g., 'dev', 'qa', 'release')
132
+ story_id: Optional story ID (format: STORY-NNN)
133
+
134
+ Returns:
135
+ Dictionary with operation context
136
+ """
137
+ try:
138
+ # Generate operation ID
139
+ operation_id = self._generate_operation_id(operation, story_id)
140
+
141
+ # Extract timing
142
+ timing = self.extract_timing()
143
+
144
+ # Extract todos
145
+ todos = self.extract_todos()
146
+
147
+ # Extract errors
148
+ errors = self.extract_errors()
149
+
150
+ # Determine status based on errors
151
+ status = "completed" if not errors else "failed"
152
+
153
+ # Build context
154
+ context = {
155
+ "operation_id": operation_id,
156
+ "operation": operation,
157
+ "story_id": story_id,
158
+ "start_time": timing.get("start_time"),
159
+ "end_time": timing.get("end_time"),
160
+ "duration": timing.get("duration"),
161
+ "status": status,
162
+ "todos": todos,
163
+ "errors": errors,
164
+ "phases": [],
165
+ }
166
+
167
+ # Limit context size
168
+ context = self.limit_context_size(context)
169
+
170
+ # Add context size
171
+ context_json = json.dumps(context)
172
+ context["context_size_bytes"] = len(context_json.encode("utf-8"))
173
+
174
+ return context
175
+
176
+ except Exception as e:
177
+ logger.warning(f"Context extraction error: {str(e)}")
178
+ # Return minimal context on error
179
+ return {
180
+ "operation_id": self._generate_operation_id(operation, story_id),
181
+ "operation": operation,
182
+ "story_id": story_id,
183
+ "status": "error",
184
+ "todos": [],
185
+ "errors": [{"message": str(e), "exception_type": type(e).__name__}],
186
+ "context_size_bytes": 0,
187
+ }
188
+
189
+ def extract_todos(self) -> List[Dict[str, Any]]:
190
+ """
191
+ Extract todos from TodoWrite.
192
+
193
+ Returns:
194
+ List of todo dictionaries with id, content, status, activeForm
195
+ """
196
+ try:
197
+ # Attempt to read TodoWrite data
198
+ # This would be from the actual TodoWrite API in production
199
+ todos = []
200
+ logger.debug(f"Extracted {len(todos)} todos")
201
+ return todos
202
+ except Exception as e:
203
+ logger.warning(f"Failed to extract todos: {str(e)}")
204
+ return []
205
+
206
+ def extract_errors(self) -> List[Dict[str, Any]]:
207
+ """
208
+ Extract error information from operation.
209
+
210
+ Returns:
211
+ List of error dictionaries with message, exception_type, stack_trace
212
+ """
213
+ try:
214
+ # Attempt to extract errors from operation logs
215
+ # This would be from actual operation logs in production
216
+ errors = []
217
+ logger.debug(f"Extracted {len(errors)} errors")
218
+ return errors
219
+ except Exception as e:
220
+ logger.warning(f"Failed to extract errors: {str(e)}")
221
+ return []
222
+
223
+ def extract_timing(self) -> Dict[str, Any]:
224
+ """
225
+ Extract timing information for operation.
226
+
227
+ Returns:
228
+ Dictionary with start_time, end_time, duration (seconds)
229
+ """
230
+ try:
231
+ now = datetime.utcnow().isoformat() + "Z"
232
+ return {
233
+ "start_time": now,
234
+ "end_time": now,
235
+ "duration": 0,
236
+ }
237
+ except Exception as e:
238
+ logger.warning(f"Failed to extract timing: {str(e)}")
239
+ return {
240
+ "start_time": None,
241
+ "end_time": None,
242
+ "duration": 0,
243
+ }
244
+
245
+ def limit_context_size(
246
+ self, context: Dict[str, Any], max_size: Optional[int] = None
247
+ ) -> Dict[str, Any]:
248
+ """
249
+ Limit context size to maximum.
250
+
251
+ If context exceeds max_size, summarizes todos and truncates errors.
252
+
253
+ Args:
254
+ context: Operation context
255
+ max_size: Maximum size in bytes (default: 50KB)
256
+
257
+ Returns:
258
+ Context limited to max_size
259
+ """
260
+ if max_size is None:
261
+ max_size = self.max_size
262
+
263
+ context_size = self._calculate_context_size(context)
264
+
265
+ if context_size <= max_size:
266
+ return context
267
+
268
+ logger.warning(
269
+ f"Context size {context_size}B exceeds limit {max_size}B, summarizing"
270
+ )
271
+
272
+ # Apply size reduction strategies
273
+ self._summarize_todos_if_needed(context)
274
+ self._truncate_errors_if_needed(context)
275
+
276
+ # Verify size after first reduction
277
+ context_size = self._calculate_context_size(context)
278
+
279
+ if context_size > max_size:
280
+ logger.warning(
281
+ f"Context still exceeds limit after summarization "
282
+ f"({context_size}B > {max_size}B), removing phases"
283
+ )
284
+ context.pop("phases", None)
285
+
286
+ return context
287
+
288
+ def _calculate_context_size(self, context: Dict[str, Any]) -> int:
289
+ """Calculate JSON size of context in bytes."""
290
+ context_json = json.dumps(context)
291
+ return len(context_json.encode("utf-8"))
292
+
293
+ def _summarize_todos_if_needed(self, context: Dict[str, Any]) -> None:
294
+ """Summarize todos if count exceeds threshold."""
295
+ todos = context.get("todos", [])
296
+ if len(todos) <= MAX_TODOS_BEFORE_SUMMARY:
297
+ return
298
+
299
+ summary = self._build_todo_summary(todos)
300
+ context["todos"] = [summary]
301
+
302
+ def _truncate_errors_if_needed(self, context: Dict[str, Any]) -> None:
303
+ """Truncate errors if count exceeds threshold."""
304
+ errors = context.get("errors", [])
305
+ if len(errors) <= MAX_ERRORS_BEFORE_TRUNCATION:
306
+ return
307
+
308
+ context["errors"] = errors[:MAX_ERRORS_BEFORE_TRUNCATION]
309
+ context["errors"].append({"message": TRUNCATION_MARKER})
310
+
311
+ @staticmethod
312
+ def _build_todo_summary(todos: List[Dict[str, Any]]) -> Dict[str, Any]:
313
+ """Build summary of todos by status."""
314
+ return {
315
+ "total": len(todos),
316
+ "completed": sum(1 for t in todos if t.get("status") == "completed"),
317
+ "in_progress": sum(1 for t in todos if t.get("status") == "in_progress"),
318
+ "pending": sum(1 for t in todos if t.get("status") == "pending"),
319
+ }
320
+
321
+ def _generate_operation_id(
322
+ self, operation: str, story_id: Optional[str] = None
323
+ ) -> str:
324
+ """
325
+ Generate unique operation ID.
326
+
327
+ Args:
328
+ operation: Operation name
329
+ story_id: Optional story ID
330
+
331
+ Returns:
332
+ Operation ID in format: operation-story-timestamp
333
+ """
334
+ timestamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
335
+ if story_id:
336
+ return f"{operation}-{story_id}-{timestamp}"
337
+ return f"{operation}-{timestamp}"
338
+
339
+
340
+ def extract_context(operation: str, story_id: Optional[str] = None) -> Dict[str, Any]:
341
+ """
342
+ Public API for context extraction.
343
+
344
+ Args:
345
+ operation: Operation name (e.g., 'dev', 'qa', 'release')
346
+ story_id: Optional story ID (format: STORY-NNN)
347
+
348
+ Returns:
349
+ Dictionary with operation context
350
+ """
351
+ extractor = ContextExtractor()
352
+ return extractor.extract_operation_context(operation, story_id)
353
+
354
+
355
+ def sanitize_context(context: Dict[str, Any]) -> Dict[str, Any]:
356
+ """
357
+ Sanitize sensitive data from context using regex patterns.
358
+
359
+ Recursively removes secrets from all string values in context.
360
+ Modifies context in place.
361
+
362
+ Args:
363
+ context: Operation context to sanitize
364
+
365
+ Returns:
366
+ Sanitized context
367
+ """
368
+ try:
369
+ _sanitize_dict(context)
370
+ return context
371
+ except Exception as e:
372
+ logger.warning(f"Context sanitization error: {str(e)}")
373
+ return context
374
+
375
+
376
+ def _sanitize_dict(obj: Any) -> None:
377
+ """
378
+ Recursively sanitize dictionary values.
379
+
380
+ Modifies object in place.
381
+
382
+ Args:
383
+ obj: Object to sanitize (dict, list, or scalar)
384
+ """
385
+ if isinstance(obj, dict):
386
+ _sanitize_dict_items(obj)
387
+ elif isinstance(obj, list):
388
+ _sanitize_list_items(obj)
389
+
390
+
391
+ def _sanitize_dict_items(obj: Dict[str, Any]) -> None:
392
+ """Sanitize all values in a dictionary."""
393
+ for key, value in obj.items():
394
+ if isinstance(value, str):
395
+ obj[key] = _sanitize_string(value)
396
+ elif isinstance(value, (dict, list)):
397
+ _sanitize_dict(value)
398
+
399
+
400
+ def _sanitize_list_items(obj: List[Any]) -> None:
401
+ """Sanitize all items in a list."""
402
+ for i, item in enumerate(obj):
403
+ if isinstance(item, str):
404
+ obj[i] = _sanitize_string(item)
405
+ elif isinstance(item, (dict, list)):
406
+ _sanitize_dict(item)
407
+
408
+
409
+ def _sanitize_string(value: str) -> str:
410
+ """
411
+ Sanitize a string value by applying all secret patterns.
412
+
413
+ Args:
414
+ value: String value to sanitize
415
+
416
+ Returns:
417
+ Sanitized string with secrets replaced with ***
418
+ """
419
+ return _apply_sanitization_patterns(value)
420
+
421
+
422
+ def _apply_sanitization_patterns(value: str) -> str:
423
+ """Apply all registered secret patterns to value."""
424
+ for pattern, replacement in SECRET_PATTERNS:
425
+ value = re.sub(pattern, replacement, value, flags=re.IGNORECASE)
426
+ return value