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.
Files changed (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. 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)