devsquad 3.6.0__py3-none-any.whl
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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
PromptAssembler - Dynamic Prompt Assembly Engine
|
|
5
|
+
|
|
6
|
+
Inspired by three prompt optimization mechanisms in the Claude Code architecture:
|
|
7
|
+
|
|
8
|
+
Inspired① Feature Flag-driven dynamic trimming:
|
|
9
|
+
Automatically select template variants with different verbosity levels
|
|
10
|
+
based on task complexity (Simple/Medium/Complex).
|
|
11
|
+
Simple tasks use 3-line concise instructions; complex tasks use enhanced
|
|
12
|
+
templates (+constraints +anti-patterns +references).
|
|
13
|
+
|
|
14
|
+
Inspired③ Compression-aware adaptation:
|
|
15
|
+
ContextCompressor's compression level (NONE/SNIP/SESSION_MEMORY/FULL_COMPACT)
|
|
16
|
+
directly influences the prompt's style and detail level, achieving
|
|
17
|
+
"more compression, more concise" self-adaptation.
|
|
18
|
+
|
|
19
|
+
Design principles:
|
|
20
|
+
- No new standalone service; embedded as an assembler within Worker._do_work()
|
|
21
|
+
- All variants derived from ROLE_TEMPLATES (original templates unchanged)
|
|
22
|
+
- Fully automatic complexity detection (based on description length/keywords/structural signals)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
import os
|
|
27
|
+
import logging
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
30
|
+
from enum import Enum
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import yaml
|
|
36
|
+
_YAML_AVAILABLE = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
_YAML_AVAILABLE = False
|
|
39
|
+
|
|
40
|
+
_RE_NUMBERING = re.compile(r'\d+[.\)、]')
|
|
41
|
+
_RE_MULTI_REQ = re.compile(r'[;;\n]')
|
|
42
|
+
|
|
43
|
+
_config_cache: Dict = {}
|
|
44
|
+
_config_cache_path: Optional[str] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TaskComplexity(Enum):
|
|
48
|
+
"""Task complexity level"""
|
|
49
|
+
SIMPLE = "simple"
|
|
50
|
+
MEDIUM = "medium"
|
|
51
|
+
COMPLEX = "complex"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class AssembledPrompt:
|
|
56
|
+
"""
|
|
57
|
+
Assembled prompt result
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
instruction: Final work instruction text
|
|
61
|
+
complexity: Detected task complexity
|
|
62
|
+
variant_used: Name of the template variant used
|
|
63
|
+
tokens_estimate: Estimated token count
|
|
64
|
+
metadata: Additional metadata (e.g., triggered keywords, trimming reasons)
|
|
65
|
+
"""
|
|
66
|
+
instruction: str
|
|
67
|
+
complexity: TaskComplexity
|
|
68
|
+
variant_used: str
|
|
69
|
+
tokens_estimate: int = 0
|
|
70
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PromptAssembler:
|
|
74
|
+
"""
|
|
75
|
+
Dynamic prompt assembler
|
|
76
|
+
|
|
77
|
+
Core flow:
|
|
78
|
+
task_description → detect_complexity() → select_template()
|
|
79
|
+
→ assemble(related_findings) → AssembledPrompt
|
|
80
|
+
|
|
81
|
+
Relationship with existing components:
|
|
82
|
+
- Worker._do_work(): Caller, passes context and gets AssembledPrompt
|
|
83
|
+
- ROLE_TEMPLATES: Variant baseline source (defined in dispatcher.py)
|
|
84
|
+
- ContextCompressor.CompressionLevel: Compression-aware input (optional)
|
|
85
|
+
|
|
86
|
+
Usage example:
|
|
87
|
+
assembler = PromptAssembler(role_id="architect", base_prompt=role_template)
|
|
88
|
+
result = assembler.assemble(task_description="Design microservice architecture",
|
|
89
|
+
related_findings=["Finding A"],
|
|
90
|
+
compression_level=CompressionLevel.NONE)
|
|
91
|
+
print(result.instruction)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
_COMPLEXITY_KEYWORDS = {
|
|
95
|
+
TaskComplexity.SIMPLE: {
|
|
96
|
+
"positive": ["write a", "create", "add", "fix bug",
|
|
97
|
+
"change a", "simple", "quick", "single function", "one line of code",
|
|
98
|
+
"small change", "complete", "format", "rename", "hello",
|
|
99
|
+
"utility class", "minor bug", "sort function", "logging",
|
|
100
|
+
"写个", "快速", "简单", "小修改", "修复", "添加", "工具函数",
|
|
101
|
+
"排序函数", "日志", "格式化", "重命名"],
|
|
102
|
+
"negative": ["architecture", "system design", "distributed", "refactor", "migration",
|
|
103
|
+
"multi-module", "full-stack", "end-to-end", "complete solution",
|
|
104
|
+
"high availability", "disaster recovery", "microservice architecture",
|
|
105
|
+
"架构", "分布式", "重构", "迁移", "微服务", "高可用", "容灾",
|
|
106
|
+
"全链路", "端到端", "完整方案"],
|
|
107
|
+
},
|
|
108
|
+
TaskComplexity.COMPLEX: {
|
|
109
|
+
"positive": ["architecture", "design pattern", "microservice", "distributed",
|
|
110
|
+
"refactor", "migration", "security audit", "performance optimization",
|
|
111
|
+
"complete solution", "system design", "tech selection",
|
|
112
|
+
"end-to-end", "full pipeline", "high availability", "disaster recovery",
|
|
113
|
+
"CI/CD", "pipeline", "comprehensive optimization",
|
|
114
|
+
"架构", "设计模式", "微服务", "分布式", "重构", "迁移",
|
|
115
|
+
"安全审计", "性能优化", "完整方案", "系统设计", "技术选型",
|
|
116
|
+
"端到端", "流水线", "高可用", "容灾", "负载均衡",
|
|
117
|
+
"服务发现", "全面优化", "全链路", "监控告警"],
|
|
118
|
+
"negative": ["write a function", "simple modification", "minor adjustment", "add a test",
|
|
119
|
+
"quick fix", "hello world",
|
|
120
|
+
"写个函数", "简单修改", "小调整", "添加测试", "快速修复"],
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_TEMPLATE_VARIANTS = {
|
|
125
|
+
TaskComplexity.SIMPLE: {
|
|
126
|
+
"name": "compact",
|
|
127
|
+
"role_truncate": 80,
|
|
128
|
+
"findings_limit": 2,
|
|
129
|
+
"findings_truncate": 60,
|
|
130
|
+
"include_constraints": False,
|
|
131
|
+
"include_anti_patterns": False,
|
|
132
|
+
"instruction_style": "direct",
|
|
133
|
+
},
|
|
134
|
+
TaskComplexity.MEDIUM: {
|
|
135
|
+
"name": "standard",
|
|
136
|
+
"role_truncate": 200,
|
|
137
|
+
"findings_limit": 5,
|
|
138
|
+
"findings_truncate": 150,
|
|
139
|
+
"include_constraints": True,
|
|
140
|
+
"include_anti_patterns": False,
|
|
141
|
+
"instruction_style": "structured",
|
|
142
|
+
},
|
|
143
|
+
TaskComplexity.COMPLEX: {
|
|
144
|
+
"name": "enhanced",
|
|
145
|
+
"role_truncate": 500,
|
|
146
|
+
"findings_limit": 8,
|
|
147
|
+
"findings_truncate": 200,
|
|
148
|
+
"include_constraints": True,
|
|
149
|
+
"include_anti_patterns": True,
|
|
150
|
+
"instruction_style": "comprehensive",
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_COMPRESSION_OVERRIDES = {
|
|
155
|
+
"NONE": {},
|
|
156
|
+
"SNIP": {
|
|
157
|
+
"role_truncate": 120,
|
|
158
|
+
"findings_limit": 3,
|
|
159
|
+
"findings_truncate": 100,
|
|
160
|
+
"include_constraints": False,
|
|
161
|
+
"include_anti_patterns": False,
|
|
162
|
+
},
|
|
163
|
+
"SESSION_MEMORY": {
|
|
164
|
+
"role_truncate": 60,
|
|
165
|
+
"findings_limit": 1,
|
|
166
|
+
"findings_truncate": 50,
|
|
167
|
+
"include_constraints": False,
|
|
168
|
+
"include_anti_patterns": False,
|
|
169
|
+
"instruction_style": "minimal",
|
|
170
|
+
},
|
|
171
|
+
"FULL_COMPACT": {
|
|
172
|
+
"role_truncate": 40,
|
|
173
|
+
"findings_limit": 0,
|
|
174
|
+
"findings_truncate": 0,
|
|
175
|
+
"include_constraints": False,
|
|
176
|
+
"include_anti_patterns": False,
|
|
177
|
+
"instruction_style": "ultra_minimal",
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
def __init__(self, role_id: str, base_prompt: str, config_path: str = None):
|
|
182
|
+
"""
|
|
183
|
+
Initialize the prompt assembler
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
role_id: Role identifier (for role-specific trimming strategies)
|
|
187
|
+
base_prompt: Base role prompt template (from ROLE_TEMPLATES)
|
|
188
|
+
config_path: Configuration file path (optional, defaults to searching for .devsquad.yaml)
|
|
189
|
+
"""
|
|
190
|
+
self.role_id = role_id
|
|
191
|
+
self.base_prompt = base_prompt
|
|
192
|
+
|
|
193
|
+
self.qc_config = self._load_config(config_path)
|
|
194
|
+
self.qc_enabled = self.qc_config.get("quality_control", {}).get("enabled", False)
|
|
195
|
+
|
|
196
|
+
self._qc_injection = ""
|
|
197
|
+
if self.qc_enabled:
|
|
198
|
+
self._qc_injection = self._build_quality_control_injection()
|
|
199
|
+
|
|
200
|
+
def _load_config(self, config_path: str = None) -> Dict:
|
|
201
|
+
"""
|
|
202
|
+
Load DevSquad configuration from YAML file.
|
|
203
|
+
|
|
204
|
+
Search order:
|
|
205
|
+
1. Explicit config_path parameter
|
|
206
|
+
2. .devsquad.yaml in current directory
|
|
207
|
+
3. .devsquad.yaml in project root (directory with pyproject.toml/.git)
|
|
208
|
+
4. Default empty config (quality control disabled)
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
config_path: Explicit path to config file
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dict: Parsed configuration dictionary
|
|
215
|
+
"""
|
|
216
|
+
if not _YAML_AVAILABLE:
|
|
217
|
+
return {"quality_control": {"enabled": False}}
|
|
218
|
+
|
|
219
|
+
global _config_cache, _config_cache_path
|
|
220
|
+
|
|
221
|
+
search_paths = []
|
|
222
|
+
|
|
223
|
+
if config_path and os.path.exists(config_path):
|
|
224
|
+
search_paths.append(config_path)
|
|
225
|
+
else:
|
|
226
|
+
current_dir = os.getcwd()
|
|
227
|
+
candidate = os.path.join(current_dir, ".devsquad.yaml")
|
|
228
|
+
if os.path.exists(candidate):
|
|
229
|
+
search_paths.append(candidate)
|
|
230
|
+
else:
|
|
231
|
+
search_dir = current_dir
|
|
232
|
+
for _ in range(5):
|
|
233
|
+
if os.path.exists(os.path.join(search_dir, "pyproject.toml")) or \
|
|
234
|
+
os.path.exists(os.path.join(search_dir, ".git")):
|
|
235
|
+
project_config = os.path.join(search_dir, ".devsquad.yaml")
|
|
236
|
+
if os.path.exists(project_config):
|
|
237
|
+
search_paths.append(project_config)
|
|
238
|
+
break
|
|
239
|
+
parent = os.path.dirname(search_dir)
|
|
240
|
+
if parent == search_dir:
|
|
241
|
+
break
|
|
242
|
+
search_dir = parent
|
|
243
|
+
|
|
244
|
+
if search_paths:
|
|
245
|
+
resolved = os.path.realpath(search_paths[0])
|
|
246
|
+
if _config_cache_path == resolved and _config_cache:
|
|
247
|
+
return _config_cache
|
|
248
|
+
try:
|
|
249
|
+
with open(resolved, 'r', encoding='utf-8') as f:
|
|
250
|
+
config = yaml.safe_load(f) or {}
|
|
251
|
+
_config_cache = config
|
|
252
|
+
_config_cache_path = resolved
|
|
253
|
+
return config
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.warning("Failed to load config from %s: %s", resolved, e)
|
|
256
|
+
return {}
|
|
257
|
+
else:
|
|
258
|
+
return {"quality_control": {"enabled": False}}
|
|
259
|
+
|
|
260
|
+
def _build_quality_control_injection(self) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Build quality control system prompt injection based on configuration.
|
|
263
|
+
|
|
264
|
+
This creates a comprehensive set of rules that will be injected into
|
|
265
|
+
every Worker's prompt, ensuring consistent quality standards.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
str: Formatted quality control instructions
|
|
269
|
+
"""
|
|
270
|
+
qc = self.qc_config.get("quality_control", {})
|
|
271
|
+
strict = qc.get("strict_mode", False)
|
|
272
|
+
|
|
273
|
+
parts = []
|
|
274
|
+
parts.append("\n\n## Quality Control System (ACTIVE)")
|
|
275
|
+
parts.append(f"Strict Mode: {'ON' if strict else 'OFF (warnings only)'}")
|
|
276
|
+
parts.append(f"Minimum Score: {qc.get('min_quality_score', 85)}/100")
|
|
277
|
+
parts.append("")
|
|
278
|
+
|
|
279
|
+
aqc = qc.get("ai_quality_control", {})
|
|
280
|
+
if aqc.get("enabled", False):
|
|
281
|
+
parts.append("### AI Quality Control Rules:")
|
|
282
|
+
|
|
283
|
+
hc = aqc.get("hallucination_check", {})
|
|
284
|
+
if hc.get("enabled", False):
|
|
285
|
+
parts.append("- **Hallucination Prevention**:")
|
|
286
|
+
if hc.get("require_traceable_references"):
|
|
287
|
+
parts.append(" . All API/library references MUST include official URL or version")
|
|
288
|
+
if hc.get("require_signature_verification"):
|
|
289
|
+
parts.append(" . Verify function signatures via `import + dir()` before using")
|
|
290
|
+
if hc.get("forbid_absolute_certainty"):
|
|
291
|
+
parts.append(" . FORBIDDEN: 'obviously', 'clearly', 'undoubtedly' - provide evidence instead")
|
|
292
|
+
|
|
293
|
+
oc = aqc.get("overconfidence_check", {})
|
|
294
|
+
if oc.get("enabled", False):
|
|
295
|
+
parts.append("- **Overconfidence Prevention**:")
|
|
296
|
+
parts.append(f" . Every technical decision MUST present >={oc.get('require_alternatives_min', 2)} alternatives with pros/cons")
|
|
297
|
+
parts.append(f" . Must list >={oc.get('require_failure_scenarios_min', 3)} potential failure scenarios")
|
|
298
|
+
if oc.get("acknowledge_tradeoffs"):
|
|
299
|
+
parts.append(" . Always acknowledge limitations and trade-offs")
|
|
300
|
+
|
|
301
|
+
pd = aqc.get("pattern_diversity", {})
|
|
302
|
+
if pd.get("enabled", False):
|
|
303
|
+
parts.append("- **Pattern Diversity**:")
|
|
304
|
+
parts.append(" . Consider current state-of-the-art (last 6 months)")
|
|
305
|
+
parts.append(" . Evaluate multiple approaches before recommending")
|
|
306
|
+
parts.append(" . Flag repeated/solutions from recent tasks")
|
|
307
|
+
|
|
308
|
+
sv = aqc.get("self_verification_prevention", {})
|
|
309
|
+
if sv.get("enabled", False):
|
|
310
|
+
parts.append("- **Self-Verification Trap Avoidance**:")
|
|
311
|
+
if sv.get("enforce_creator_tester_separation"):
|
|
312
|
+
parts.append(" . Code creator and test creator MUST be different roles")
|
|
313
|
+
if sv.get("require_spec_based_testing"):
|
|
314
|
+
parts.append(" . Tests based on specification (PRD), NOT implementation details")
|
|
315
|
+
parts.append(f" . Error case coverage >={sv.get('min_error_coverage_percent', 15)}%")
|
|
316
|
+
parts.append("")
|
|
317
|
+
|
|
318
|
+
asg = qc.get("ai_security_guard", {})
|
|
319
|
+
if asg.get("enabled", False):
|
|
320
|
+
parts.append("### Security Rules (PermissionGuard):")
|
|
321
|
+
perm_level = asg.get("permission_level", "DEFAULT")
|
|
322
|
+
level_desc = {
|
|
323
|
+
"PLAN": "Read-only mode (no file modifications)",
|
|
324
|
+
"DEFAULT": "Write ops require confirmation",
|
|
325
|
+
"AUTO": "AI auto-judges safe operations (trusted context)",
|
|
326
|
+
"BYPASS": "Full skip (manual authorization required)"
|
|
327
|
+
}
|
|
328
|
+
parts.append(f"- Current Level: **L1/L2/L3/L4[{perm_level}]**: {level_desc.get(perm_level, 'Unknown')}")
|
|
329
|
+
|
|
330
|
+
iv = asg.get("input_validation", {})
|
|
331
|
+
if iv.get("enabled", False):
|
|
332
|
+
parts.append("- **Input Validation (16 patterns active)**:")
|
|
333
|
+
if iv.get("block_high_severity"):
|
|
334
|
+
parts.append(" . BLOCK: SQL/Command/XSS/SSRF/Path injection -> immediate rejection")
|
|
335
|
+
if iv.get("warn_and_sanitize_medium"):
|
|
336
|
+
parts.append(" . SANITIZE: LDAP/XPath/Header/Email injection -> cleaned + warning")
|
|
337
|
+
if iv.get("flag_low_severity"):
|
|
338
|
+
parts.append(" . FLAG: Template/ReDoS/Format/XXE -> advisory warning")
|
|
339
|
+
|
|
340
|
+
parts.append("- **Sensitive Data Rules**:")
|
|
341
|
+
parts.append(" . FORBIDDEN: Write passwords/keys/tokens to Scratchpad SHARED zone")
|
|
342
|
+
parts.append(" . FORBIDDEN: Include secrets in error messages or logs")
|
|
343
|
+
parts.append(" . REQUIRED: Use environment variables or secret managers for credentials")
|
|
344
|
+
parts.append("")
|
|
345
|
+
|
|
346
|
+
atc = qc.get("ai_team_collaboration", {})
|
|
347
|
+
if atc.get("enabled", False):
|
|
348
|
+
parts.append("### Collaboration Rules:")
|
|
349
|
+
|
|
350
|
+
raci = atc.get("raci", {})
|
|
351
|
+
if raci.get("mode") == "strict":
|
|
352
|
+
parts.append("- **RACI Matrix (STRICT mode)**:")
|
|
353
|
+
parts.append(" . One Responsible (R) per task - the primary doer")
|
|
354
|
+
parts.append(" . One Accountable (A) per task - final owner/approver")
|
|
355
|
+
parts.append(" . Consulted (C) roles must be asked BEFORE decisions")
|
|
356
|
+
parts.append(" . Informed (I) roles notified AFTER decisions")
|
|
357
|
+
|
|
358
|
+
scratchpad = atc.get("scratchpad", {})
|
|
359
|
+
if scratchpad.get("protocol") == "zoned":
|
|
360
|
+
parts.append("- **Scratchpad Zoned Protocol**:")
|
|
361
|
+
parts.append(" . READONLY zone: Other roles' outputs (read-only, no modify)")
|
|
362
|
+
parts.append(" . WRITE zone: Your output only (isolated namespace)")
|
|
363
|
+
parts.append(" . SHARED zone: Consensus-approved conclusions (requires vote)")
|
|
364
|
+
parts.append(" . PRIVATE zone: Sensitive data (invisible to others)")
|
|
365
|
+
|
|
366
|
+
consensus = atc.get("consensus", {})
|
|
367
|
+
if consensus.get("enabled", False):
|
|
368
|
+
parts.append(f"- **Consensus Mechanism** (threshold: {consensus.get('threshold', 0.7)*100:.0f}%):")
|
|
369
|
+
parts.append(" . Weighted voting by role importance")
|
|
370
|
+
if consensus.get("veto_enabled"):
|
|
371
|
+
veto_roles = consensus.get("veto_allowed_roles", [])
|
|
372
|
+
parts.append(f" . Veto power: {', '.join(veto_roles) if veto_roles else 'None'}")
|
|
373
|
+
parts.append(" . Deadlock: Auto-escalate to user after timeout")
|
|
374
|
+
|
|
375
|
+
parts.append("")
|
|
376
|
+
|
|
377
|
+
min_score = qc.get("min_quality_score", 85)
|
|
378
|
+
parts.append("### Output Quality Gate:")
|
|
379
|
+
parts.append(f"- Your output will be scored (0-{min_score-1} REJECTED / {min_score}-99 CONDITIONAL / 100 ACCEPTED)")
|
|
380
|
+
parts.append("- Low score triggers specific improvement requirements")
|
|
381
|
+
if strict:
|
|
382
|
+
parts.append("- **In STRICT mode: Rejected outputs cannot proceed to next phase**")
|
|
383
|
+
|
|
384
|
+
return "\n".join(parts)
|
|
385
|
+
|
|
386
|
+
def detect_complexity(self, task_description: str) -> TaskComplexity:
|
|
387
|
+
"""
|
|
388
|
+
Automatically detect task complexity
|
|
389
|
+
|
|
390
|
+
Three-dimensional scoring model:
|
|
391
|
+
1. Length dimension: <30 chars -> Simple, 30~150 chars -> Medium, >150 chars -> Complex
|
|
392
|
+
2. Keyword dimension: Match SIMPLE/COMPLEX keyword groups
|
|
393
|
+
3. Structure dimension: Whether it contains numbered lists/multiple questions/multi-layer requirements
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
task_description: Task description text
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
TaskComplexity: Detected complexity level
|
|
400
|
+
"""
|
|
401
|
+
desc_lower = task_description.lower()
|
|
402
|
+
desc_len = len(task_description)
|
|
403
|
+
|
|
404
|
+
score_simple = 0.0
|
|
405
|
+
score_complex = 0.0
|
|
406
|
+
|
|
407
|
+
length_score = 0.0
|
|
408
|
+
if desc_len < 15:
|
|
409
|
+
length_score = -0.5
|
|
410
|
+
elif desc_len < 30:
|
|
411
|
+
length_score = -0.3
|
|
412
|
+
elif desc_len < 150:
|
|
413
|
+
length_score = 0.0
|
|
414
|
+
else:
|
|
415
|
+
length_score = 0.3
|
|
416
|
+
|
|
417
|
+
simple_kw = self._COMPLEXITY_KEYWORDS[TaskComplexity.SIMPLE]
|
|
418
|
+
complex_kw = self._COMPLEXITY_KEYWORDS[TaskComplexity.COMPLEX]
|
|
419
|
+
|
|
420
|
+
def _word_match(keyword: str, text: str) -> bool:
|
|
421
|
+
if '\u4e00' <= keyword[0] <= '\u9fff':
|
|
422
|
+
return keyword in text
|
|
423
|
+
return bool(re.search(r'\b' + re.escape(keyword) + r'\b', text))
|
|
424
|
+
|
|
425
|
+
for kw in simple_kw["positive"]:
|
|
426
|
+
if _word_match(kw, desc_lower):
|
|
427
|
+
score_simple += 0.15
|
|
428
|
+
for kw in simple_kw["negative"]:
|
|
429
|
+
if _word_match(kw, desc_lower):
|
|
430
|
+
score_simple -= 0.2
|
|
431
|
+
|
|
432
|
+
for kw in complex_kw["positive"]:
|
|
433
|
+
if _word_match(kw, desc_lower):
|
|
434
|
+
score_complex += 0.2
|
|
435
|
+
for kw in complex_kw["negative"]:
|
|
436
|
+
if _word_match(kw, desc_lower):
|
|
437
|
+
score_complex -= 0.15
|
|
438
|
+
|
|
439
|
+
has_numbering = bool(_RE_NUMBERING.search(task_description))
|
|
440
|
+
has_multi_question = task_description.count('?') >= 2
|
|
441
|
+
has_multi_requirement = len(_RE_MULTI_REQ.split(task_description)) >= 3
|
|
442
|
+
|
|
443
|
+
structure_bonus = 0.0
|
|
444
|
+
if has_numbering:
|
|
445
|
+
structure_bonus += 0.1
|
|
446
|
+
if has_multi_question:
|
|
447
|
+
structure_bonus += 0.15
|
|
448
|
+
if has_multi_requirement:
|
|
449
|
+
structure_bonus += 0.1
|
|
450
|
+
|
|
451
|
+
final_simple = score_simple + length_score * 0.5
|
|
452
|
+
final_complex = score_complex + length_score * 0.5 + structure_bonus
|
|
453
|
+
|
|
454
|
+
if not task_description.strip():
|
|
455
|
+
return TaskComplexity.SIMPLE
|
|
456
|
+
if desc_len < 15:
|
|
457
|
+
return TaskComplexity.SIMPLE
|
|
458
|
+
if final_complex > 0.3 and final_complex > final_simple + 0.1:
|
|
459
|
+
return TaskComplexity.COMPLEX
|
|
460
|
+
if final_simple > 0.15 and final_simple > final_complex + 0.05:
|
|
461
|
+
return TaskComplexity.SIMPLE
|
|
462
|
+
return TaskComplexity.MEDIUM
|
|
463
|
+
|
|
464
|
+
def assemble(self,
|
|
465
|
+
task_description: str,
|
|
466
|
+
related_findings: List[str] = None,
|
|
467
|
+
task_id: str = "",
|
|
468
|
+
compression_level=None) -> AssembledPrompt:
|
|
469
|
+
"""
|
|
470
|
+
Assemble the final prompt
|
|
471
|
+
|
|
472
|
+
Complete flow:
|
|
473
|
+
1. Detect task complexity
|
|
474
|
+
2. Select base template variant
|
|
475
|
+
3. Apply compression level overrides (if any)
|
|
476
|
+
4. Trim each section according to configuration
|
|
477
|
+
5. Assemble final instruction
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
task_description: Task description
|
|
481
|
+
related_findings: Related findings list (from Scratchpad)
|
|
482
|
+
task_id: Task ID (for instruction header)
|
|
483
|
+
compression_level: ContextCompressor compression level (optional)
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
AssembledPrompt: Assembly result, containing instruction/complexity/variant/metadata
|
|
487
|
+
"""
|
|
488
|
+
complexity = self.detect_complexity(task_description)
|
|
489
|
+
config = dict(self._TEMPLATE_VARIANTS[complexity])
|
|
490
|
+
|
|
491
|
+
if compression_level is not None:
|
|
492
|
+
override_key = compression_level.name if hasattr(compression_level, 'name') else str(compression_level).upper()
|
|
493
|
+
override = self._COMPRESSION_OVERRIDES.get(override_key, {})
|
|
494
|
+
config.update(override)
|
|
495
|
+
|
|
496
|
+
role_display = self.base_prompt[:config["role_truncate"]]
|
|
497
|
+
findings_to_include = (related_findings or [])[:config["findings_limit"]]
|
|
498
|
+
truncated_findings = [
|
|
499
|
+
f[:config["findings_truncate"]] for f in findings_to_include
|
|
500
|
+
]
|
|
501
|
+
|
|
502
|
+
style = config.get("instruction_style", "structured")
|
|
503
|
+
instruction = self._build_instruction(
|
|
504
|
+
style=style,
|
|
505
|
+
task_id=task_id,
|
|
506
|
+
task_description=task_description,
|
|
507
|
+
role_display=role_display,
|
|
508
|
+
findings=truncated_findings,
|
|
509
|
+
include_constraints=config.get("include_constraints", False),
|
|
510
|
+
include_anti_patterns=config.get("include_anti_patterns", False),
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
token_est = len(instruction) // 3
|
|
514
|
+
|
|
515
|
+
return AssembledPrompt(
|
|
516
|
+
instruction=instruction,
|
|
517
|
+
complexity=complexity,
|
|
518
|
+
variant_used=config.get("name", f"{complexity.value}_custom"),
|
|
519
|
+
tokens_estimate=token_est,
|
|
520
|
+
metadata={
|
|
521
|
+
"compression_applied": compression_level is not None,
|
|
522
|
+
"compression_level": str(compression_level),
|
|
523
|
+
"original_base_length": len(self.base_prompt),
|
|
524
|
+
"assembled_length": len(instruction),
|
|
525
|
+
"findings_included": len(truncated_findings),
|
|
526
|
+
"findings_total": len(related_findings or []),
|
|
527
|
+
},
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
def _build_instruction(self,
|
|
531
|
+
style: str,
|
|
532
|
+
task_id: str,
|
|
533
|
+
task_description: str,
|
|
534
|
+
role_display: str,
|
|
535
|
+
findings: List[str],
|
|
536
|
+
include_constraints: bool,
|
|
537
|
+
include_anti_patterns: bool) -> str:
|
|
538
|
+
"""
|
|
539
|
+
Build work instruction in the specified style
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
style: Instruction style (direct/structured/comprehensive/minimal/ultra_minimal)
|
|
543
|
+
task_id: Task ID
|
|
544
|
+
task_description: Task description
|
|
545
|
+
role_display: Trimmed role prompt
|
|
546
|
+
findings: Trimmed related findings list
|
|
547
|
+
include_constraints: Whether to include constraint reminders
|
|
548
|
+
include_anti_patterns: Whether to include anti-pattern warnings
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
str: Assembled instruction text
|
|
552
|
+
"""
|
|
553
|
+
if style == "ultra_minimal":
|
|
554
|
+
base = (
|
|
555
|
+
f"[{self.role_id}] {task_description}\n"
|
|
556
|
+
f"Output core conclusion."
|
|
557
|
+
)
|
|
558
|
+
return base + (self._qc_injection if self.qc_enabled and self._qc_injection else "")
|
|
559
|
+
|
|
560
|
+
if style == "minimal":
|
|
561
|
+
parts = [f"[{self.role_id}] Task: {task_description}"]
|
|
562
|
+
if findings:
|
|
563
|
+
parts.append(f"Reference: {findings[0][:50]}")
|
|
564
|
+
user_rules = self._get_user_rules_injection(task_description)
|
|
565
|
+
if user_rules:
|
|
566
|
+
parts.append(f"Rules: {user_rules[:100]}")
|
|
567
|
+
parts.append("Output key conclusion.")
|
|
568
|
+
base = "\n".join(parts)
|
|
569
|
+
return base + (self._qc_injection if self.qc_enabled and self._qc_injection else "")
|
|
570
|
+
|
|
571
|
+
if style == "direct":
|
|
572
|
+
user_rules = self._get_user_rules_injection(task_description)
|
|
573
|
+
base = (
|
|
574
|
+
f"=== Task ===\n"
|
|
575
|
+
f"Description: {task_description}\n"
|
|
576
|
+
f"Role: {role_display}...\n\n"
|
|
577
|
+
+ (f"=== Related Findings ===\n" +
|
|
578
|
+
"\n".join(f"- {f}" for f in findings) + "\n\n" if findings else "") +
|
|
579
|
+
(f"=== User Rules ===\n{user_rules}\n\n" if user_rules else "") +
|
|
580
|
+
"Complete your work, output core conclusion."
|
|
581
|
+
)
|
|
582
|
+
return base + (self._qc_injection if self.qc_enabled and self._qc_injection else "")
|
|
583
|
+
|
|
584
|
+
parts = []
|
|
585
|
+
parts.append(f"=== Task ===")
|
|
586
|
+
if task_id:
|
|
587
|
+
parts.append(f"Task ID: {task_id}")
|
|
588
|
+
parts.append(f"Description: {task_description}")
|
|
589
|
+
parts.append(f"Role: {role_display}")
|
|
590
|
+
parts.append("")
|
|
591
|
+
|
|
592
|
+
if findings:
|
|
593
|
+
parts.append("=== Related Findings (from other Workers) ===")
|
|
594
|
+
for i, f in enumerate(findings, 1):
|
|
595
|
+
parts.append(f" {i}. {f}")
|
|
596
|
+
parts.append("")
|
|
597
|
+
|
|
598
|
+
if include_constraints:
|
|
599
|
+
parts.append("=== Constraints ===")
|
|
600
|
+
parts.append("- Output must be actionable and verifiable")
|
|
601
|
+
parts.append("- Mark assumptions and risk points")
|
|
602
|
+
parts.append("")
|
|
603
|
+
|
|
604
|
+
if include_anti_patterns:
|
|
605
|
+
anti_patterns = self._get_role_anti_patterns()
|
|
606
|
+
if anti_patterns:
|
|
607
|
+
parts.append("=== Anti-Pattern Warnings ===")
|
|
608
|
+
for ap in anti_patterns:
|
|
609
|
+
parts.append(f"- Avoid: {ap}")
|
|
610
|
+
parts.append("")
|
|
611
|
+
|
|
612
|
+
parts.append("Please complete your work based on the above information.")
|
|
613
|
+
if style == "comprehensive":
|
|
614
|
+
parts.append("Output should include: analysis process, key decisions, specific plan, risk assessment.")
|
|
615
|
+
else:
|
|
616
|
+
parts.append("Output your core findings (1-3 key conclusions).")
|
|
617
|
+
|
|
618
|
+
user_rules = self._get_user_rules_injection(task_description)
|
|
619
|
+
if user_rules:
|
|
620
|
+
parts.append("")
|
|
621
|
+
parts.append("=== User Rules (from natural language collection) ===")
|
|
622
|
+
parts.append(user_rules)
|
|
623
|
+
|
|
624
|
+
if self.qc_enabled and self._qc_injection:
|
|
625
|
+
parts.append(self._qc_injection)
|
|
626
|
+
|
|
627
|
+
ar_content = self._get_anti_rationalization_injection()
|
|
628
|
+
if ar_content:
|
|
629
|
+
parts.append(ar_content)
|
|
630
|
+
|
|
631
|
+
return "\n".join(parts)
|
|
632
|
+
|
|
633
|
+
def _get_user_rules_injection(self, task_description: str) -> str:
|
|
634
|
+
"""Query user rules from RuleCollector storage and format as prompt text."""
|
|
635
|
+
try:
|
|
636
|
+
from scripts.collaboration.rule_collector import RuleStorage
|
|
637
|
+
if not hasattr(self, '_rule_storage'):
|
|
638
|
+
self._rule_storage = RuleStorage.get_shared()
|
|
639
|
+
keywords = self._extract_keywords(task_description)
|
|
640
|
+
rules = self._rule_storage.query(
|
|
641
|
+
trigger_keywords=keywords, min_confidence=0.5
|
|
642
|
+
)
|
|
643
|
+
if not rules:
|
|
644
|
+
return ""
|
|
645
|
+
lines = []
|
|
646
|
+
for r in rules[:10]:
|
|
647
|
+
rtype = r.get("type", "always")
|
|
648
|
+
trigger = r.get("trigger", "")
|
|
649
|
+
action = r.get("action", "")
|
|
650
|
+
if rtype == "forbid":
|
|
651
|
+
lines.append(f"FORBIDDEN: {trigger + ' -> ' if trigger else ''}{action}")
|
|
652
|
+
elif rtype == "avoid":
|
|
653
|
+
lines.append(f"AVOID: {trigger + ' -> ' if trigger else ''}{action}")
|
|
654
|
+
elif rtype == "always":
|
|
655
|
+
lines.append(f"ALWAYS: {trigger + ' -> ' if trigger else ''}{action}")
|
|
656
|
+
elif rtype == "prefer":
|
|
657
|
+
lines.append(f"PREFER: {trigger + ' -> ' if trigger else ''}{action}")
|
|
658
|
+
return "\n".join(lines)
|
|
659
|
+
except Exception:
|
|
660
|
+
return ""
|
|
661
|
+
|
|
662
|
+
_STOP_WORDS = frozenset({
|
|
663
|
+
"the", "is", "to", "of", "it", "in", "on", "at", "by", "an", "be",
|
|
664
|
+
"do", "or", "as", "if", "so", "no", "not", "but", "and", "for",
|
|
665
|
+
"with", "this", "that", "from", "are", "was", "were", "been", "have",
|
|
666
|
+
"has", "had", "will", "would", "could", "should", "may", "might",
|
|
667
|
+
"can", "shall", "a", "i", "you", "he", "she", "we", "they", "me",
|
|
668
|
+
"him", "her", "us", "them", "my", "your", "his", "its", "our",
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
@staticmethod
|
|
672
|
+
def _extract_keywords(text: str, max_keywords: int = 8) -> List[str]:
|
|
673
|
+
"""Extract keywords from text, supporting both CJK and Latin scripts."""
|
|
674
|
+
keywords = []
|
|
675
|
+
for w in text.split():
|
|
676
|
+
if len(w) > 1 and w.lower() not in PromptAssembler._STOP_WORDS:
|
|
677
|
+
keywords.append(w)
|
|
678
|
+
has_cjk = any('\u4e00' <= ch <= '\u9fff' for ch in text)
|
|
679
|
+
if has_cjk:
|
|
680
|
+
cjk_segments = re.findall(r'[\u4e00-\u9fff]{2,}', text)
|
|
681
|
+
for seg in cjk_segments:
|
|
682
|
+
for i in range(0, len(seg) - 1, 2):
|
|
683
|
+
keywords.append(seg[i:i + 2])
|
|
684
|
+
return keywords[:max_keywords]
|
|
685
|
+
|
|
686
|
+
def _get_role_anti_patterns(self) -> List[str]:
|
|
687
|
+
"""
|
|
688
|
+
Get role-specific anti-pattern warning list
|
|
689
|
+
|
|
690
|
+
Different roles have different common anti-patterns.
|
|
691
|
+
|
|
692
|
+
Returns:
|
|
693
|
+
List[str]: List of anti-patterns this role should avoid
|
|
694
|
+
"""
|
|
695
|
+
patterns = {
|
|
696
|
+
"architect": [
|
|
697
|
+
"Over-engineering (YAGNI violation)",
|
|
698
|
+
"Ignoring non-functional requirements (performance/security/ops)",
|
|
699
|
+
"Tech selection based only on popularity without considering team capability",
|
|
700
|
+
],
|
|
701
|
+
"tester": [
|
|
702
|
+
"Only writing happy path tests",
|
|
703
|
+
"Tests disconnected from business requirements",
|
|
704
|
+
"Excessive mocking making tests meaningless",
|
|
705
|
+
],
|
|
706
|
+
"solo-coder": [
|
|
707
|
+
"Skipping design and jumping to coding",
|
|
708
|
+
"Not handling edge cases",
|
|
709
|
+
"Hardcoded configuration and magic numbers",
|
|
710
|
+
],
|
|
711
|
+
"product_manager": [
|
|
712
|
+
"Vague requirements leading to repeated changes",
|
|
713
|
+
"Priority confusion",
|
|
714
|
+
"Ignoring technical feasibility",
|
|
715
|
+
],
|
|
716
|
+
"ui-designer": [
|
|
717
|
+
"Only creating visual mockups without considering interaction states",
|
|
718
|
+
"Ignoring responsive design and accessibility",
|
|
719
|
+
"Inconsistent design system",
|
|
720
|
+
],
|
|
721
|
+
}
|
|
722
|
+
return patterns.get(self.role_id, [])
|
|
723
|
+
|
|
724
|
+
def _get_anti_rationalization_injection(self) -> str:
|
|
725
|
+
"""
|
|
726
|
+
Inject AntiRationalizationEngine content into prompt (P0-1).
|
|
727
|
+
|
|
728
|
+
Loads per-role excuse->rebuttal table and formats as markdown.
|
|
729
|
+
This is the primary defense against Workers skipping quality steps.
|
|
730
|
+
|
|
731
|
+
Returns:
|
|
732
|
+
str: Formatted AR table, or empty string if unavailable
|
|
733
|
+
"""
|
|
734
|
+
try:
|
|
735
|
+
from scripts.collaboration.anti_rationalization import get_shared_engine
|
|
736
|
+
if not hasattr(self, '_ar_engine'):
|
|
737
|
+
self._ar_engine = get_shared_engine()
|
|
738
|
+
return self._ar_engine.format_for_prompt(self.role_id)
|
|
739
|
+
except Exception as e:
|
|
740
|
+
logger.debug("AntiRationalizationEngine not available: %s", e)
|
|
741
|
+
return ""
|
|
742
|
+
|
|
743
|
+
@staticmethod
|
|
744
|
+
def estimate_tokens(text: str) -> int:
|
|
745
|
+
"""
|
|
746
|
+
Roughly estimate the token count of text
|
|
747
|
+
|
|
748
|
+
In mixed Chinese/English scenarios, approximately 3 characters = 1 token.
|
|
749
|
+
|
|
750
|
+
Args:
|
|
751
|
+
text: Text to estimate
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
int: Estimated token count
|
|
755
|
+
"""
|
|
756
|
+
return max(1, len(text) // 3)
|