ref-agents 1.0.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 (175) hide show
  1. ref_agents/__init__.py +9 -0
  2. ref_agents/api_keys.json.example +8 -0
  3. ref_agents/auth.py +129 -0
  4. ref_agents/codemap/..md +62 -0
  5. ref_agents/codemap/CODE_MAP.md +37 -0
  6. ref_agents/codemap/core.md +43 -0
  7. ref_agents/codemap/models.md +43 -0
  8. ref_agents/codemap/prompts.md +40 -0
  9. ref_agents/codemap/security.md +45 -0
  10. ref_agents/codemap/tools.md +94 -0
  11. ref_agents/codemap/tools_browser.md +44 -0
  12. ref_agents/codemap/utils.md +42 -0
  13. ref_agents/codemap/workflow.md +42 -0
  14. ref_agents/config/ai_patterns.yaml +101 -0
  15. ref_agents/config/frameworks/angular.yaml +104 -0
  16. ref_agents/config/frameworks/aspnet.yaml +84 -0
  17. ref_agents/config/frameworks/ef_core.yaml +81 -0
  18. ref_agents/config/frameworks/react.yaml +111 -0
  19. ref_agents/config/frameworks/spring_boot.yaml +117 -0
  20. ref_agents/config/languages/csharp.yaml +153 -0
  21. ref_agents/config/languages/java.yaml +188 -0
  22. ref_agents/config/languages/javascript.yaml +172 -0
  23. ref_agents/config/languages/python.yaml +153 -0
  24. ref_agents/config/languages/typescript.yaml +193 -0
  25. ref_agents/constants.py +553 -0
  26. ref_agents/core/__init__.py +15 -0
  27. ref_agents/core/config_loader.py +160 -0
  28. ref_agents/core/config_models.py +167 -0
  29. ref_agents/core/config_parsing.py +84 -0
  30. ref_agents/core/language_detector.py +388 -0
  31. ref_agents/core/validation_models.py +66 -0
  32. ref_agents/core/validation_primitives.py +176 -0
  33. ref_agents/errors.py +34 -0
  34. ref_agents/license_client.py +247 -0
  35. ref_agents/models/__init__.py +22 -0
  36. ref_agents/models/gherkin.py +45 -0
  37. ref_agents/models/hierarchy.py +80 -0
  38. ref_agents/models/invest.py +59 -0
  39. ref_agents/models/version.py +49 -0
  40. ref_agents/prompts/__init__.py +9 -0
  41. ref_agents/prompts/start_agent.py +772 -0
  42. ref_agents/rules/architecture/backend_patterns.md +43 -0
  43. ref_agents/rules/architecture/diagramming.md +100 -0
  44. ref_agents/rules/architecture/frontend_patterns.md +40 -0
  45. ref_agents/rules/architecture/impact_analysis.md +129 -0
  46. ref_agents/rules/architecture/migration_strategy.md +208 -0
  47. ref_agents/rules/architecture/regression_protocol.md +77 -0
  48. ref_agents/rules/architecture/system_design.md +97 -0
  49. ref_agents/rules/common/codemap_standard.md +97 -0
  50. ref_agents/rules/common/core_protocol.md +59 -0
  51. ref_agents/rules/common/prompt_engineering.md +294 -0
  52. ref_agents/rules/development/debugging.md +32 -0
  53. ref_agents/rules/development/implementation.md +205 -0
  54. ref_agents/rules/operations/completion.md +119 -0
  55. ref_agents/rules/operations/cutover_protocol.md +218 -0
  56. ref_agents/rules/operations/discovery.md +179 -0
  57. ref_agents/rules/operations/fix_workflow.md +87 -0
  58. ref_agents/rules/operations/forensics.md +278 -0
  59. ref_agents/rules/operations/platform.md +263 -0
  60. ref_agents/rules/operations/synchronous_flow.md +25 -0
  61. ref_agents/rules/product/ac_validation.md +25 -0
  62. ref_agents/rules/product/brainstorming.md +27 -0
  63. ref_agents/rules/product/ref_flow.md +101 -0
  64. ref_agents/rules/product/requirements_std.md +114 -0
  65. ref_agents/rules/product/spec_writing.md +235 -0
  66. ref_agents/rules/product/strategy.md +96 -0
  67. ref_agents/rules/quality/documentation_standards.md +46 -0
  68. ref_agents/rules/quality/parity_testing.md +234 -0
  69. ref_agents/rules/quality/project_documentation.md +56 -0
  70. ref_agents/rules/quality/qa_lead.md +111 -0
  71. ref_agents/rules/quality/test_design.md +146 -0
  72. ref_agents/rules/quality/testing_standards.md +293 -0
  73. ref_agents/rules/review/pr_review.md +116 -0
  74. ref_agents/rules/security/security_audit.md +83 -0
  75. ref_agents/security/__init__.py +22 -0
  76. ref_agents/security/dependency_audit.py +188 -0
  77. ref_agents/security/file_audit.py +208 -0
  78. ref_agents/security/network_scan.py +179 -0
  79. ref_agents/security/report_generator.py +313 -0
  80. ref_agents/security/secret_scan.py +252 -0
  81. ref_agents/security/url_scan.py +240 -0
  82. ref_agents/security_scan.py +236 -0
  83. ref_agents/server.py +1586 -0
  84. ref_agents/session.py +100 -0
  85. ref_agents/tool_names.py +55 -0
  86. ref_agents/tools/__init__.py +8 -0
  87. ref_agents/tools/agents_generator.py +315 -0
  88. ref_agents/tools/ai_pattern_detector.py +815 -0
  89. ref_agents/tools/brownfield_populator.py +529 -0
  90. ref_agents/tools/browser/__init__.py +50 -0
  91. ref_agents/tools/browser/evidence_verifier.py +302 -0
  92. ref_agents/tools/browser/execution_logger.py +249 -0
  93. ref_agents/tools/browser/playwright_mcp_client.py +259 -0
  94. ref_agents/tools/browser/screenshot_utils.py +184 -0
  95. ref_agents/tools/browser/test_executor.py +537 -0
  96. ref_agents/tools/code_quality_scanner.py +629 -0
  97. ref_agents/tools/codemap/..md +93 -0
  98. ref_agents/tools/codemap/CODE_MAP.md +30 -0
  99. ref_agents/tools/codemap/browser.md +44 -0
  100. ref_agents/tools/codemap.py +403 -0
  101. ref_agents/tools/codemap_freshness.py +234 -0
  102. ref_agents/tools/comment_smell_scanner.py +346 -0
  103. ref_agents/tools/complexity.py +436 -0
  104. ref_agents/tools/complexity_ast.py +333 -0
  105. ref_agents/tools/compliance.py +246 -0
  106. ref_agents/tools/compliance_remediation.py +846 -0
  107. ref_agents/tools/context_graph.py +839 -0
  108. ref_agents/tools/context_manager.py +550 -0
  109. ref_agents/tools/context_tools.py +121 -0
  110. ref_agents/tools/cross_repo_linker.py +393 -0
  111. ref_agents/tools/dead_code_scanner.py +637 -0
  112. ref_agents/tools/debt_scanner.py +1092 -0
  113. ref_agents/tools/dependency_graph.py +272 -0
  114. ref_agents/tools/discovery_audit.py +372 -0
  115. ref_agents/tools/docs_scanner.py +600 -0
  116. ref_agents/tools/evaluate_gate.py +119 -0
  117. ref_agents/tools/external_detector.py +524 -0
  118. ref_agents/tools/features_generator.py +282 -0
  119. ref_agents/tools/flow_gap_detector.py +373 -0
  120. ref_agents/tools/flow_mapper.py +327 -0
  121. ref_agents/tools/full_suite_runner.py +740 -0
  122. ref_agents/tools/gherkin_parser.py +227 -0
  123. ref_agents/tools/guard_tools.py +139 -0
  124. ref_agents/tools/handoff_tools.py +282 -0
  125. ref_agents/tools/health_scanner.py +1211 -0
  126. ref_agents/tools/hierarchy_manager.py +289 -0
  127. ref_agents/tools/invest_scorer.py +249 -0
  128. ref_agents/tools/jira_confluence_export.py +306 -0
  129. ref_agents/tools/json_output.py +76 -0
  130. ref_agents/tools/migration_mapper.py +946 -0
  131. ref_agents/tools/migration_readiness_scanner.py +209 -0
  132. ref_agents/tools/pattern_learner.py +522 -0
  133. ref_agents/tools/report_utils.py +155 -0
  134. ref_agents/tools/requirements_serializer.py +225 -0
  135. ref_agents/tools/security_audit_tool.py +106 -0
  136. ref_agents/tools/sequencing_engine.py +288 -0
  137. ref_agents/tools/summary_generator.py +275 -0
  138. ref_agents/tools/symbol_resolver.py +306 -0
  139. ref_agents/tools/symbol_smoke_runner.py +336 -0
  140. ref_agents/tools/test_plan_validator.py +189 -0
  141. ref_agents/tools/test_smell_walker.py +902 -0
  142. ref_agents/tools/tier1_fixer.py +502 -0
  143. ref_agents/tools/validators/__init__.py +419 -0
  144. ref_agents/tools/validators/architect.py +268 -0
  145. ref_agents/tools/validators/cutover_engineer.py +167 -0
  146. ref_agents/tools/validators/developer.py +180 -0
  147. ref_agents/tools/validators/discovery.py +150 -0
  148. ref_agents/tools/validators/forensic_engineer.py +191 -0
  149. ref_agents/tools/validators/impact_architect.py +181 -0
  150. ref_agents/tools/validators/migration_planner.py +166 -0
  151. ref_agents/tools/validators/parity_tester.py +155 -0
  152. ref_agents/tools/validators/platform_engineer.py +134 -0
  153. ref_agents/tools/validators/pr_reviewer.py +129 -0
  154. ref_agents/tools/validators/product_manager.py +291 -0
  155. ref_agents/tools/validators/qa_lead.py +172 -0
  156. ref_agents/tools/validators/scrum_master.py +212 -0
  157. ref_agents/tools/validators/security_owner.py +162 -0
  158. ref_agents/tools/validators/specifier.py +134 -0
  159. ref_agents/tools/validators/strategist.py +149 -0
  160. ref_agents/tools/validators/tester.py +121 -0
  161. ref_agents/tools/version_manager.py +202 -0
  162. ref_agents/tools/workflow_tools.py +1549 -0
  163. ref_agents/utils/__init__.py +21 -0
  164. ref_agents/utils/git_utils.py +351 -0
  165. ref_agents/utils/handoff_logger.py +368 -0
  166. ref_agents/utils/ignore_matcher.py +270 -0
  167. ref_agents/workflow/__init__.py +19 -0
  168. ref_agents/workflow/capabilities.py +328 -0
  169. ref_agents/workflow/state_machine.py +708 -0
  170. ref_agents/workflow/transitions.py +658 -0
  171. ref_agents-1.0.0.dist-info/METADATA +365 -0
  172. ref_agents-1.0.0.dist-info/RECORD +175 -0
  173. ref_agents-1.0.0.dist-info/WHEEL +4 -0
  174. ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
  175. ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
@@ -0,0 +1,227 @@
1
+ """Gherkin parser for converting text to structured Gherkin models."""
2
+
3
+ import re
4
+ from typing import Literal, Optional, cast
5
+
6
+ import structlog
7
+
8
+ from ref_agents.models.gherkin import GherkinScenario, GherkinStep
9
+
10
+ logger = structlog.get_logger(__name__)
11
+
12
+ GherkinKeyword = Literal["Given", "When", "Then", "And", "But"]
13
+
14
+ # Gherkin keyword patterns
15
+ GHERKIN_KEYWORDS: list[GherkinKeyword] = ["Given", "When", "Then", "And", "But"]
16
+ GHERKIN_KEYWORDS_LOWER = [kw.lower() for kw in GHERKIN_KEYWORDS]
17
+
18
+
19
+ def parse_gherkin_text(text: str) -> list[GherkinScenario]:
20
+ """Parse Gherkin text into structured scenarios.
21
+
22
+ Args:
23
+ text: Gherkin-formatted text (can include multiple scenarios)
24
+
25
+ Returns:
26
+ List of GherkinScenario objects
27
+
28
+ Example:
29
+ text = '''
30
+ Scenario: User login
31
+ Given a user exists
32
+ When the user logs in
33
+ Then the user is authenticated
34
+ '''
35
+ """
36
+ scenarios = []
37
+ lines = text.strip().split("\n")
38
+
39
+ current_scenario: Optional[GherkinScenario] = None
40
+ current_steps: list[GherkinStep] = []
41
+
42
+ for line in lines:
43
+ line = line.strip()
44
+ if not line:
45
+ continue
46
+
47
+ # Check for scenario title
48
+ if line.lower().startswith("scenario:"):
49
+ # Save previous scenario if exists
50
+ if current_scenario:
51
+ current_scenario.steps = current_steps
52
+ scenarios.append(current_scenario)
53
+
54
+ scenario_title = line.split(":", 1)[1].strip()
55
+ current_scenario = GherkinScenario(scenario=scenario_title)
56
+ current_steps = []
57
+
58
+ # Check for Gherkin step
59
+ elif _is_gherkin_step(line):
60
+ step = _parse_step(line)
61
+ if step:
62
+ current_steps.append(step)
63
+
64
+ # Save last scenario
65
+ if current_scenario:
66
+ current_scenario.steps = current_steps
67
+ scenarios.append(current_scenario)
68
+ elif current_steps:
69
+ # If no scenario title, create default scenario
70
+ scenarios.append(
71
+ GherkinScenario(scenario="Default Scenario", steps=current_steps)
72
+ )
73
+
74
+ return scenarios
75
+
76
+
77
+ def parse_markdown_ac(markdown_text: str) -> list[GherkinScenario]:
78
+ """Parse acceptance criteria from markdown format.
79
+
80
+ Handles formats like:
81
+ - **AC-1**: Given X, When Y, Then Z
82
+ - ### AC-1: Title
83
+ **Given** X
84
+ **When** Y
85
+ **Then** Z
86
+
87
+ Args:
88
+ markdown_text: Markdown-formatted acceptance criteria
89
+
90
+ Returns:
91
+ List of GherkinScenario objects
92
+ """
93
+ scenarios = []
94
+
95
+ # Pattern for AC with title: ### AC-1: Title
96
+ ac_title_pattern = r"^###\s*(AC-\d+|AC\d+):\s*(.+)$"
97
+ # Pattern for bold keywords: **Given** text
98
+ bold_keyword_pattern = r"\*\*(Given|When|Then|And|But)\*\*\s*(.+)$"
99
+
100
+ lines = markdown_text.strip().split("\n")
101
+ current_scenario: Optional[GherkinScenario] = None
102
+ current_steps: list[GherkinStep] = []
103
+
104
+ for line in lines:
105
+ line = line.strip()
106
+ if not line:
107
+ continue
108
+
109
+ # Check for AC title
110
+ title_match = re.match(ac_title_pattern, line, re.IGNORECASE)
111
+ if title_match:
112
+ # Save previous scenario
113
+ if current_scenario:
114
+ current_scenario.steps = current_steps
115
+ scenarios.append(current_scenario)
116
+
117
+ ac_id = title_match.group(1)
118
+ ac_title = title_match.group(2).strip()
119
+ current_scenario = GherkinScenario(scenario=f"{ac_id}: {ac_title}")
120
+ current_steps = []
121
+
122
+ # Check for bold keyword (markdown format)
123
+ elif current_scenario:
124
+ bold_match = re.match(bold_keyword_pattern, line, re.IGNORECASE)
125
+ if bold_match:
126
+ keyword = cast(GherkinKeyword, bold_match.group(1).capitalize())
127
+ text = bold_match.group(2).strip()
128
+ current_steps.append(GherkinStep(keyword=keyword, text=text))
129
+ # Check for inline format: **AC1**: Given X, When Y, Then Z (even without current scenario)
130
+ elif ":" in line and any(kw.lower() in line.lower() for kw in GHERKIN_KEYWORDS):
131
+ inline_scenario = _parse_inline_ac(line)
132
+ if inline_scenario:
133
+ scenarios.append(inline_scenario)
134
+
135
+ # Save last scenario
136
+ if current_scenario:
137
+ current_scenario.steps = current_steps
138
+ scenarios.append(current_scenario)
139
+
140
+ return scenarios
141
+
142
+
143
+ def validate_gherkin_syntax(text: str) -> tuple[bool, Optional[str]]:
144
+ """Validate Gherkin syntax.
145
+
146
+ Args:
147
+ text: Gherkin text to validate
148
+
149
+ Returns:
150
+ Tuple of (is_valid, error_message)
151
+ """
152
+ scenarios = parse_gherkin_text(text)
153
+
154
+ if not scenarios:
155
+ return False, "No scenarios found"
156
+
157
+ for scenario in scenarios:
158
+ if not scenario.steps:
159
+ return False, f"Scenario '{scenario.scenario}' has no steps"
160
+
161
+ # Check for at least one Given/When/Then
162
+ has_given = any(step.keyword == "Given" for step in scenario.steps)
163
+ has_when = any(step.keyword == "When" for step in scenario.steps)
164
+
165
+ if not (has_given or has_when):
166
+ return (
167
+ False,
168
+ f"Scenario '{scenario.scenario}' must have at least one Given or When step",
169
+ )
170
+
171
+ return True, None
172
+
173
+
174
+ def _is_gherkin_step(line: str) -> bool:
175
+ """Check if line starts with a Gherkin keyword."""
176
+ line_lower = line.lower().strip()
177
+ return any(line_lower.startswith(kw.lower()) for kw in GHERKIN_KEYWORDS)
178
+
179
+
180
+ def _parse_step(line: str) -> Optional[GherkinStep]:
181
+ """Parse a single Gherkin step line."""
182
+ line = line.strip()
183
+
184
+ # Find keyword
185
+ keyword = None
186
+ for kw in GHERKIN_KEYWORDS:
187
+ if line.lower().startswith(kw.lower()):
188
+ keyword = kw
189
+ break
190
+
191
+ if not keyword:
192
+ return None
193
+
194
+ # Extract text (remove keyword)
195
+ text = line[len(keyword) :].strip()
196
+ return GherkinStep(keyword=keyword, text=text)
197
+
198
+
199
+ def _parse_inline_ac(line: str) -> Optional[GherkinScenario]:
200
+ """Parse inline AC format: **AC1**: Given X, When Y, Then Z."""
201
+ # Pattern: **AC-1** or **AC1**: Given X, When Y, Then Z
202
+ pattern = r"\*\*(AC[- ]?\d+)\*\*:\s*(.+)$"
203
+ match = re.match(pattern, line, re.IGNORECASE)
204
+
205
+ if not match:
206
+ return None
207
+
208
+ ac_id = match.group(1)
209
+ steps_text = match.group(2)
210
+
211
+ # Parse steps from comma-separated or newline-separated format
212
+ steps = []
213
+ # Try splitting by comma first
214
+ if "," in steps_text:
215
+ parts = [p.strip() for p in steps_text.split(",")]
216
+ else:
217
+ parts = [steps_text.strip()]
218
+
219
+ for part in parts:
220
+ step = _parse_step(part)
221
+ if step:
222
+ steps.append(step)
223
+
224
+ if steps:
225
+ return GherkinScenario(scenario=f"{ac_id}", steps=steps)
226
+
227
+ return None
@@ -0,0 +1,139 @@
1
+ from pathlib import Path
2
+ import structlog
3
+ from ref_agents.constants import Status, PHASE_CONFIG, AVAILABLE_AGENTS
4
+ from ref_agents.workflow.state_machine import WorkflowManager
5
+ from ref_agents.tools.context_tools import find_project_root
6
+
7
+ logger = structlog.get_logger(__name__)
8
+
9
+ # Pre-compute agent requirements from PHASE_CONFIG for performance
10
+ _AGENT_REQUIREMENTS: dict[str, list[str]] = {}
11
+ for phase in PHASE_CONFIG.values():
12
+ _AGENT_REQUIREMENTS[phase["primary_agent"]] = phase["required_artifacts"]
13
+ # Handle parallel/background agents if needed, though usually they share phase reqs
14
+ if "parallel_agent" in phase:
15
+ _AGENT_REQUIREMENTS[phase["parallel_agent"]] = phase["required_artifacts"]
16
+
17
+
18
+ def _check_codemap_exists() -> bool:
19
+ """Check if CODE_MAP.md exists using priority-based detection.
20
+
21
+ Detection priority (industry best practice):
22
+ 1. Session project_root (explicit user setting via set_project_root)
23
+ 2. CWD (current working directory - primary for test isolation)
24
+ 3. CWD parent traversal (find project root)
25
+ 4. Secondary endpoint: docs/CODE_MAP.md in root locations
26
+
27
+ Returns:
28
+ True if CODE_MAP.md found in any checked location.
29
+ """
30
+ from ref_agents.session import SessionManager
31
+
32
+ # Priority 1: Explicit session project_root (highest priority)
33
+ # Verify path still exists (handles stale session state)
34
+ session_root = SessionManager.get().project_root
35
+ if session_root and session_root.exists():
36
+ if (session_root / "CODE_MAP.md").exists():
37
+ logger.debug("codemap_found", source="session_root", path=str(session_root))
38
+ return True
39
+ # Secondary endpoint: docs/CODE_MAP.md
40
+ if (session_root / "docs" / "CODE_MAP.md").exists():
41
+ logger.debug(
42
+ "codemap_found",
43
+ source="session_root_docs",
44
+ path=str(session_root / "docs" / "CODE_MAP.md"),
45
+ )
46
+ return True
47
+
48
+ # Priority 2: CWD (important for test isolation)
49
+ cwd = Path.cwd()
50
+ if (cwd / "CODE_MAP.md").exists():
51
+ logger.debug("codemap_found", source="cwd", path=str(cwd))
52
+ return True
53
+ # Secondary endpoint: docs/CODE_MAP.md in CWD
54
+ if (cwd / "docs" / "CODE_MAP.md").exists():
55
+ logger.debug(
56
+ "codemap_found", source="cwd_docs", path=str(cwd / "docs" / "CODE_MAP.md")
57
+ )
58
+ return True
59
+
60
+ # Priority 3: CWD parent traversal (find project root)
61
+ for parent in cwd.parents:
62
+ if (parent / "CODE_MAP.md").exists():
63
+ logger.debug("codemap_found", source="cwd_parent", path=str(parent))
64
+ return True
65
+ # Secondary endpoint: docs/CODE_MAP.md in parent
66
+ if (parent / "docs" / "CODE_MAP.md").exists():
67
+ logger.debug(
68
+ "codemap_found",
69
+ source="cwd_parent_docs",
70
+ path=str(parent / "docs" / "CODE_MAP.md"),
71
+ )
72
+ return True
73
+
74
+ logger.debug(
75
+ "codemap_not_found", checked_session=str(session_root), checked_cwd=str(cwd)
76
+ )
77
+ return False
78
+
79
+
80
+ def _check_artifact_exists(artifact: str) -> bool:
81
+ """Check if an artifact exists using found root."""
82
+ if artifact == "CODE_MAP.md":
83
+ return _check_codemap_exists()
84
+
85
+ root = find_project_root()
86
+ if not root:
87
+ root = Path.cwd()
88
+
89
+ if artifact == "requirements":
90
+ req_dir = root / "docs/requirements"
91
+ return req_dir.exists() and any(req_dir.glob("*.md"))
92
+
93
+ # Handle wildcards correctly
94
+ if artifact == "requirements/*.md":
95
+ req_dir = root / "docs/requirements"
96
+ return req_dir.exists() and any(req_dir.glob("*.md"))
97
+
98
+ if artifact in ("design", "implementation", "ac_validated", "review_passed"):
99
+ manager = WorkflowManager()
100
+ active = manager.get_active_story()
101
+ if active:
102
+ state = manager.get_state(active)
103
+ if state:
104
+ return state.artifacts.get(artifact, False)
105
+ return False
106
+
107
+ # Handle regular file paths (e.g., docs/ARCHITECTURE.md)
108
+ artifact_path = root / artifact
109
+ return artifact_path.exists()
110
+
111
+
112
+ def _check_agent_artifacts(agent_name: str) -> tuple[bool, list[str]]:
113
+ """Check required artifacts for agent."""
114
+ required = _AGENT_REQUIREMENTS.get(agent_name, [])
115
+ missing = [art for art in required if not _check_artifact_exists(art)]
116
+ return len(missing) == 0, missing
117
+
118
+
119
+ def check_agent_prerequisites_impl(agent_name: str) -> str:
120
+ """Implementation of check_agent_prerequisites tool."""
121
+ if agent_name not in AVAILABLE_AGENTS:
122
+ return f"{Status.ERROR} Invalid agent: {agent_name}"
123
+
124
+ all_exist, missing = _check_agent_artifacts(agent_name)
125
+
126
+ if all_exist:
127
+ return f"{Status.SUCCESS} All prerequisites met for `{agent_name}`."
128
+
129
+ lines = [
130
+ f"{Status.ERROR} Prerequisites not met for `{agent_name}`",
131
+ "",
132
+ "Missing artifacts:",
133
+ ]
134
+ for art in missing:
135
+ lines.append(f" - {art}")
136
+
137
+ lines.append("")
138
+ lines.append("Complete these before activating the agent.")
139
+ return "\n".join(lines)
@@ -0,0 +1,282 @@
1
+ """Handoff Tools for REF Agents.
2
+
3
+ Consolidated logging for workflow events: escalation, ownership, replan, debt, phase.
4
+ """
5
+
6
+ import uuid
7
+
8
+ from ref_agents.constants import Status
9
+ from ref_agents.session import SessionManager
10
+ from ref_agents.utils import handoff_logger
11
+
12
+ # Valid event types
13
+ EVENT_TYPES = ("escalation", "ownership", "replan", "debt", "phase")
14
+
15
+
16
+ def _log_escalation_event( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
17
+ level: str,
18
+ reason: str,
19
+ to_agents: str,
20
+ from_agent: str,
21
+ active_agent: str,
22
+ story_id: str | None,
23
+ ) -> str:
24
+ """Handle escalation event."""
25
+ agents_list = [a.strip() for a in to_agents.split(",") if a.strip()]
26
+ src_agent = from_agent if from_agent else active_agent
27
+ success = handoff_logger.log_escalation(
28
+ level=level,
29
+ from_agent=src_agent,
30
+ to_agents=agents_list,
31
+ reason=reason,
32
+ story_id=story_id,
33
+ )
34
+ if success:
35
+ return (
36
+ f"{Status.SUCCESS} Escalation logged\n"
37
+ f"Level: {level}\n"
38
+ f"From: {src_agent} → To: {', '.join(agents_list)}\n"
39
+ f"Reason: {reason}"
40
+ )
41
+ return f"{Status.ERROR} Failed to log escalation"
42
+
43
+
44
+ def _log_ownership_event(
45
+ story_id: str,
46
+ active_agent: str,
47
+ to_owner: str,
48
+ reason: str,
49
+ ) -> str:
50
+ """Handle ownership transfer event."""
51
+ success = handoff_logger.log_ownership_transfer(
52
+ story_id=story_id,
53
+ from_owner=active_agent,
54
+ to_owner=to_owner,
55
+ reason=reason,
56
+ )
57
+ if success:
58
+ return (
59
+ f"{Status.SUCCESS} Ownership transfer logged\n"
60
+ f"Story: {story_id}\n"
61
+ f"From: {active_agent} → To: {to_owner}\n"
62
+ f"Reason: {reason}"
63
+ )
64
+ return f"{Status.ERROR} Failed to log ownership transfer"
65
+
66
+
67
+ def _log_replan_event(story_id: str, trigger: str, impact: str) -> str:
68
+ """Handle replan event."""
69
+ success = handoff_logger.log_replan_triggered(
70
+ story_id=story_id,
71
+ trigger=trigger,
72
+ impact=impact,
73
+ )
74
+ if success:
75
+ return (
76
+ f"{Status.SUCCESS} Replan logged\n"
77
+ f"Story: {story_id}\n"
78
+ f"Trigger: {trigger}\n"
79
+ f"Impact: {impact}"
80
+ )
81
+ return f"{Status.ERROR} Failed to log replan"
82
+
83
+
84
+ def _log_debt_event(
85
+ debt_type: str, severity: str, location: str, story_id: str | None
86
+ ) -> str:
87
+ """Handle debt event."""
88
+ debt_id = f"DEBT-{str(uuid.uuid4())[:8]}"
89
+ success = handoff_logger.log_debt_found(
90
+ debt_id=debt_id,
91
+ location=location,
92
+ debt_type=debt_type,
93
+ severity=severity,
94
+ story_id=story_id,
95
+ )
96
+ if success:
97
+ return (
98
+ f"{Status.SUCCESS} Debt logged: {debt_id}\n"
99
+ f"Type: {debt_type} | Severity: {severity}\n"
100
+ f"Location: {location}"
101
+ )
102
+ return f"{Status.ERROR} Failed to log debt"
103
+
104
+
105
+ def _log_phase_event(story_id: str, phase: str, outcome: str, next_phase: str) -> str:
106
+ """Handle phase event."""
107
+ next_p = next_phase if next_phase else None
108
+ success = handoff_logger.log_phase_complete(story_id, phase, outcome, next_p)
109
+ if success:
110
+ return (
111
+ f"{Status.SUCCESS} Phase completion logged\n"
112
+ f"Story: {story_id}\n"
113
+ f"Phase: {phase} → {outcome}\n"
114
+ f"Next: {next_phase or 'None'}"
115
+ )
116
+ return f"{Status.ERROR} Failed to log phase completion"
117
+
118
+
119
+ def log_event( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
120
+ event_type: str,
121
+ story_id: str = "",
122
+ level: str = "",
123
+ reason: str = "",
124
+ to_agents: str = "",
125
+ from_agent: str = "",
126
+ to_owner: str = "",
127
+ trigger: str = "",
128
+ impact: str = "",
129
+ debt_type: str = "",
130
+ severity: str = "",
131
+ location: str = "",
132
+ phase: str = "",
133
+ outcome: str = "",
134
+ next_phase: str = "",
135
+ ) -> str:
136
+ """Log a workflow event.
137
+
138
+ Args:
139
+ event_type: One of: escalation, ownership, replan, debt, phase.
140
+ story_id: Story identifier.
141
+ level: Escalation level (L1, L2, L3) - for escalation events.
142
+ reason: Reason for event - for escalation, ownership events.
143
+ to_agents: Comma-separated target agents - for escalation events.
144
+ from_agent: Source agent - for escalation events.
145
+ to_owner: New owner - for ownership events.
146
+ trigger: Replan trigger - for replan events.
147
+ impact: Replan impact - for replan events.
148
+ debt_type: Debt type - for debt events.
149
+ severity: Debt severity - for debt events.
150
+ location: Debt location - for debt events.
151
+ phase: Phase name - for phase events.
152
+ outcome: Phase outcome - for phase events.
153
+ next_phase: Next phase - for phase events.
154
+
155
+ Returns:
156
+ Success/error message.
157
+ """
158
+ if event_type not in EVENT_TYPES:
159
+ return f"{Status.ERROR} Invalid event_type. Use: {', '.join(EVENT_TYPES)}"
160
+
161
+ active_agent = SessionManager.get().active_agent or "unknown"
162
+ story = story_id if story_id else None
163
+
164
+ if event_type == "escalation":
165
+ return _log_escalation_event(
166
+ level, reason, to_agents, from_agent, active_agent, story
167
+ )
168
+
169
+ if event_type == "ownership":
170
+ return _log_ownership_event(story_id, active_agent, to_owner, reason)
171
+
172
+ if event_type == "replan":
173
+ return _log_replan_event(story_id, trigger, impact)
174
+
175
+ if event_type == "debt":
176
+ return _log_debt_event(debt_type, severity, location, story)
177
+
178
+ if event_type == "phase":
179
+ return _log_phase_event(story_id, phase, outcome, next_phase)
180
+
181
+ return f"{Status.ERROR} Unknown event type: {event_type}"
182
+
183
+
184
+ def log_phase_completion_impl(
185
+ story_id: str, phase: str, outcome: str, next_phase: str = ""
186
+ ) -> str:
187
+ """Log phase completion event (wrapper for MCP tool).
188
+
189
+ Args:
190
+ story_id: Story identifier.
191
+ phase: Phase that was completed.
192
+ outcome: Outcome (passed, failed).
193
+ next_phase: Next phase in flow.
194
+
195
+ Returns:
196
+ Success/error message.
197
+ """
198
+ return log_event(
199
+ event_type="phase",
200
+ story_id=story_id,
201
+ phase=phase,
202
+ outcome=outcome,
203
+ next_phase=next_phase,
204
+ )
205
+
206
+
207
+ def log_phase_complete_impl(
208
+ story_id: str, phase: str, outcome: str, next_phase: str = ""
209
+ ) -> str:
210
+ """Log phase completion event (alias for compatibility).
211
+
212
+ Args:
213
+ story_id: Story identifier.
214
+ phase: Phase that was completed.
215
+ outcome: Outcome (passed, failed).
216
+ next_phase: Next phase in flow.
217
+
218
+ Returns:
219
+ Success/error message.
220
+ """
221
+ return log_phase_completion_impl(story_id, phase, outcome, next_phase)
222
+
223
+
224
+ # Backward compatibility wrappers for server.py
225
+ def log_escalation_impl(
226
+ level: str, reason: str, to_agents: str, story_id: str = ""
227
+ ) -> str:
228
+ """Log escalation event (wrapper)."""
229
+ return log_event(
230
+ event_type="escalation",
231
+ level=level,
232
+ reason=reason,
233
+ to_agents=to_agents,
234
+ story_id=story_id,
235
+ )
236
+
237
+
238
+ def log_ownership_transfer_impl(story_id: str, to_owner: str, reason: str) -> str:
239
+ """Log ownership transfer event (wrapper)."""
240
+ return log_event(
241
+ event_type="ownership",
242
+ story_id=story_id,
243
+ to_owner=to_owner,
244
+ reason=reason,
245
+ )
246
+
247
+
248
+ def log_replan_triggered_impl(story_id: str, trigger: str, impact: str) -> str:
249
+ """Log replan triggered event (wrapper)."""
250
+ return log_event(
251
+ event_type="replan",
252
+ story_id=story_id,
253
+ trigger=trigger,
254
+ impact=impact,
255
+ )
256
+
257
+
258
+ def log_debt_found_impl(
259
+ debt_type: str, severity: str, location: str, story_id: str = ""
260
+ ) -> str:
261
+ """Log debt found event (wrapper)."""
262
+ return log_event(
263
+ event_type="debt",
264
+ debt_type=debt_type,
265
+ severity=severity,
266
+ location=location,
267
+ story_id=story_id,
268
+ )
269
+
270
+
271
+ def log_escalation_event_impl( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
272
+ level: str, from_agent: str, to_agents: str, reason: str, story_id: str = ""
273
+ ) -> str:
274
+ """Log escalation event with explicit from_agent (wrapper)."""
275
+ return log_event(
276
+ event_type="escalation",
277
+ level=level,
278
+ from_agent=from_agent,
279
+ to_agents=to_agents,
280
+ reason=reason,
281
+ story_id=story_id,
282
+ )