devforgeai 1.0.5 → 1.0.7
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/bin/devforgeai.js +0 -0
- package/package.json +9 -1
- package/src/CLAUDE.md +699 -0
- package/src/claude/hooks/phase-completion-gate.sh +0 -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/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,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
|