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.
- ref_agents/__init__.py +9 -0
- ref_agents/api_keys.json.example +8 -0
- ref_agents/auth.py +129 -0
- ref_agents/codemap/..md +62 -0
- ref_agents/codemap/CODE_MAP.md +37 -0
- ref_agents/codemap/core.md +43 -0
- ref_agents/codemap/models.md +43 -0
- ref_agents/codemap/prompts.md +40 -0
- ref_agents/codemap/security.md +45 -0
- ref_agents/codemap/tools.md +94 -0
- ref_agents/codemap/tools_browser.md +44 -0
- ref_agents/codemap/utils.md +42 -0
- ref_agents/codemap/workflow.md +42 -0
- ref_agents/config/ai_patterns.yaml +101 -0
- ref_agents/config/frameworks/angular.yaml +104 -0
- ref_agents/config/frameworks/aspnet.yaml +84 -0
- ref_agents/config/frameworks/ef_core.yaml +81 -0
- ref_agents/config/frameworks/react.yaml +111 -0
- ref_agents/config/frameworks/spring_boot.yaml +117 -0
- ref_agents/config/languages/csharp.yaml +153 -0
- ref_agents/config/languages/java.yaml +188 -0
- ref_agents/config/languages/javascript.yaml +172 -0
- ref_agents/config/languages/python.yaml +153 -0
- ref_agents/config/languages/typescript.yaml +193 -0
- ref_agents/constants.py +553 -0
- ref_agents/core/__init__.py +15 -0
- ref_agents/core/config_loader.py +160 -0
- ref_agents/core/config_models.py +167 -0
- ref_agents/core/config_parsing.py +84 -0
- ref_agents/core/language_detector.py +388 -0
- ref_agents/core/validation_models.py +66 -0
- ref_agents/core/validation_primitives.py +176 -0
- ref_agents/errors.py +34 -0
- ref_agents/license_client.py +247 -0
- ref_agents/models/__init__.py +22 -0
- ref_agents/models/gherkin.py +45 -0
- ref_agents/models/hierarchy.py +80 -0
- ref_agents/models/invest.py +59 -0
- ref_agents/models/version.py +49 -0
- ref_agents/prompts/__init__.py +9 -0
- ref_agents/prompts/start_agent.py +772 -0
- ref_agents/rules/architecture/backend_patterns.md +43 -0
- ref_agents/rules/architecture/diagramming.md +100 -0
- ref_agents/rules/architecture/frontend_patterns.md +40 -0
- ref_agents/rules/architecture/impact_analysis.md +129 -0
- ref_agents/rules/architecture/migration_strategy.md +208 -0
- ref_agents/rules/architecture/regression_protocol.md +77 -0
- ref_agents/rules/architecture/system_design.md +97 -0
- ref_agents/rules/common/codemap_standard.md +97 -0
- ref_agents/rules/common/core_protocol.md +59 -0
- ref_agents/rules/common/prompt_engineering.md +294 -0
- ref_agents/rules/development/debugging.md +32 -0
- ref_agents/rules/development/implementation.md +205 -0
- ref_agents/rules/operations/completion.md +119 -0
- ref_agents/rules/operations/cutover_protocol.md +218 -0
- ref_agents/rules/operations/discovery.md +179 -0
- ref_agents/rules/operations/fix_workflow.md +87 -0
- ref_agents/rules/operations/forensics.md +278 -0
- ref_agents/rules/operations/platform.md +263 -0
- ref_agents/rules/operations/synchronous_flow.md +25 -0
- ref_agents/rules/product/ac_validation.md +25 -0
- ref_agents/rules/product/brainstorming.md +27 -0
- ref_agents/rules/product/ref_flow.md +101 -0
- ref_agents/rules/product/requirements_std.md +114 -0
- ref_agents/rules/product/spec_writing.md +235 -0
- ref_agents/rules/product/strategy.md +96 -0
- ref_agents/rules/quality/documentation_standards.md +46 -0
- ref_agents/rules/quality/parity_testing.md +234 -0
- ref_agents/rules/quality/project_documentation.md +56 -0
- ref_agents/rules/quality/qa_lead.md +111 -0
- ref_agents/rules/quality/test_design.md +146 -0
- ref_agents/rules/quality/testing_standards.md +293 -0
- ref_agents/rules/review/pr_review.md +116 -0
- ref_agents/rules/security/security_audit.md +83 -0
- ref_agents/security/__init__.py +22 -0
- ref_agents/security/dependency_audit.py +188 -0
- ref_agents/security/file_audit.py +208 -0
- ref_agents/security/network_scan.py +179 -0
- ref_agents/security/report_generator.py +313 -0
- ref_agents/security/secret_scan.py +252 -0
- ref_agents/security/url_scan.py +240 -0
- ref_agents/security_scan.py +236 -0
- ref_agents/server.py +1586 -0
- ref_agents/session.py +100 -0
- ref_agents/tool_names.py +55 -0
- ref_agents/tools/__init__.py +8 -0
- ref_agents/tools/agents_generator.py +315 -0
- ref_agents/tools/ai_pattern_detector.py +815 -0
- ref_agents/tools/brownfield_populator.py +529 -0
- ref_agents/tools/browser/__init__.py +50 -0
- ref_agents/tools/browser/evidence_verifier.py +302 -0
- ref_agents/tools/browser/execution_logger.py +249 -0
- ref_agents/tools/browser/playwright_mcp_client.py +259 -0
- ref_agents/tools/browser/screenshot_utils.py +184 -0
- ref_agents/tools/browser/test_executor.py +537 -0
- ref_agents/tools/code_quality_scanner.py +629 -0
- ref_agents/tools/codemap/..md +93 -0
- ref_agents/tools/codemap/CODE_MAP.md +30 -0
- ref_agents/tools/codemap/browser.md +44 -0
- ref_agents/tools/codemap.py +403 -0
- ref_agents/tools/codemap_freshness.py +234 -0
- ref_agents/tools/comment_smell_scanner.py +346 -0
- ref_agents/tools/complexity.py +436 -0
- ref_agents/tools/complexity_ast.py +333 -0
- ref_agents/tools/compliance.py +246 -0
- ref_agents/tools/compliance_remediation.py +846 -0
- ref_agents/tools/context_graph.py +839 -0
- ref_agents/tools/context_manager.py +550 -0
- ref_agents/tools/context_tools.py +121 -0
- ref_agents/tools/cross_repo_linker.py +393 -0
- ref_agents/tools/dead_code_scanner.py +637 -0
- ref_agents/tools/debt_scanner.py +1092 -0
- ref_agents/tools/dependency_graph.py +272 -0
- ref_agents/tools/discovery_audit.py +372 -0
- ref_agents/tools/docs_scanner.py +600 -0
- ref_agents/tools/evaluate_gate.py +119 -0
- ref_agents/tools/external_detector.py +524 -0
- ref_agents/tools/features_generator.py +282 -0
- ref_agents/tools/flow_gap_detector.py +373 -0
- ref_agents/tools/flow_mapper.py +327 -0
- ref_agents/tools/full_suite_runner.py +740 -0
- ref_agents/tools/gherkin_parser.py +227 -0
- ref_agents/tools/guard_tools.py +139 -0
- ref_agents/tools/handoff_tools.py +282 -0
- ref_agents/tools/health_scanner.py +1211 -0
- ref_agents/tools/hierarchy_manager.py +289 -0
- ref_agents/tools/invest_scorer.py +249 -0
- ref_agents/tools/jira_confluence_export.py +306 -0
- ref_agents/tools/json_output.py +76 -0
- ref_agents/tools/migration_mapper.py +946 -0
- ref_agents/tools/migration_readiness_scanner.py +209 -0
- ref_agents/tools/pattern_learner.py +522 -0
- ref_agents/tools/report_utils.py +155 -0
- ref_agents/tools/requirements_serializer.py +225 -0
- ref_agents/tools/security_audit_tool.py +106 -0
- ref_agents/tools/sequencing_engine.py +288 -0
- ref_agents/tools/summary_generator.py +275 -0
- ref_agents/tools/symbol_resolver.py +306 -0
- ref_agents/tools/symbol_smoke_runner.py +336 -0
- ref_agents/tools/test_plan_validator.py +189 -0
- ref_agents/tools/test_smell_walker.py +902 -0
- ref_agents/tools/tier1_fixer.py +502 -0
- ref_agents/tools/validators/__init__.py +419 -0
- ref_agents/tools/validators/architect.py +268 -0
- ref_agents/tools/validators/cutover_engineer.py +167 -0
- ref_agents/tools/validators/developer.py +180 -0
- ref_agents/tools/validators/discovery.py +150 -0
- ref_agents/tools/validators/forensic_engineer.py +191 -0
- ref_agents/tools/validators/impact_architect.py +181 -0
- ref_agents/tools/validators/migration_planner.py +166 -0
- ref_agents/tools/validators/parity_tester.py +155 -0
- ref_agents/tools/validators/platform_engineer.py +134 -0
- ref_agents/tools/validators/pr_reviewer.py +129 -0
- ref_agents/tools/validators/product_manager.py +291 -0
- ref_agents/tools/validators/qa_lead.py +172 -0
- ref_agents/tools/validators/scrum_master.py +212 -0
- ref_agents/tools/validators/security_owner.py +162 -0
- ref_agents/tools/validators/specifier.py +134 -0
- ref_agents/tools/validators/strategist.py +149 -0
- ref_agents/tools/validators/tester.py +121 -0
- ref_agents/tools/version_manager.py +202 -0
- ref_agents/tools/workflow_tools.py +1549 -0
- ref_agents/utils/__init__.py +21 -0
- ref_agents/utils/git_utils.py +351 -0
- ref_agents/utils/handoff_logger.py +368 -0
- ref_agents/utils/ignore_matcher.py +270 -0
- ref_agents/workflow/__init__.py +19 -0
- ref_agents/workflow/capabilities.py +328 -0
- ref_agents/workflow/state_machine.py +708 -0
- ref_agents/workflow/transitions.py +658 -0
- ref_agents-1.0.0.dist-info/METADATA +365 -0
- ref_agents-1.0.0.dist-info/RECORD +175 -0
- ref_agents-1.0.0.dist-info/WHEEL +4 -0
- ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|