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,368 @@
|
|
|
1
|
+
"""Handoff event logger for REF Agents.
|
|
2
|
+
|
|
3
|
+
Logs ownership transfers, escalations, and other handoff events
|
|
4
|
+
to handoff_log.jsonl for audit trail.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
|
|
14
|
+
from ref_agents.utils.git_utils import get_current_user_id
|
|
15
|
+
|
|
16
|
+
logger = structlog.get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _write_graph_node(
|
|
20
|
+
node_type: str,
|
|
21
|
+
content: str,
|
|
22
|
+
story_id: str | None,
|
|
23
|
+
metadata: dict[str, str],
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Write a node to the context graph from a handoff event.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
node_type: Valid ContextGraph NodeType string (issue, decision, agent_session…).
|
|
29
|
+
content: One-sentence description of the event.
|
|
30
|
+
story_id: Active story identifier (empty string if unknown).
|
|
31
|
+
metadata: Additional key/value metadata attached to the node.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
from datetime import datetime, timezone
|
|
35
|
+
|
|
36
|
+
from ref_agents.tools.context_graph import ContextNode, get_context_graph
|
|
37
|
+
|
|
38
|
+
graph = get_context_graph()
|
|
39
|
+
ts = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S%f")
|
|
40
|
+
node = ContextNode(
|
|
41
|
+
id=f"{node_type}-hl-{ts}",
|
|
42
|
+
node_type=node_type, # type: ignore[arg-type]
|
|
43
|
+
content=content,
|
|
44
|
+
story_id=story_id or "",
|
|
45
|
+
metadata=metadata,
|
|
46
|
+
)
|
|
47
|
+
graph.add_node(node)
|
|
48
|
+
graph.save()
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.warning("handoff_graph_write_failed", error=str(e))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
EventType = Literal[
|
|
54
|
+
"ownership_transfer",
|
|
55
|
+
"agent_activation",
|
|
56
|
+
"escalation",
|
|
57
|
+
"escalation_resolved",
|
|
58
|
+
"replan_triggered",
|
|
59
|
+
"debt_logged",
|
|
60
|
+
"phase_complete",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_log_path(project_root: Path | None = None) -> Path:
|
|
65
|
+
"""Get path to handoff log file.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
project_root: Project root directory. Defaults to session project_root
|
|
69
|
+
if set, otherwise cwd.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Path to handoff_log.jsonl.
|
|
73
|
+
"""
|
|
74
|
+
if project_root is None:
|
|
75
|
+
from ref_agents.session import SessionManager
|
|
76
|
+
|
|
77
|
+
session = SessionManager.get()
|
|
78
|
+
project_root = session.project_root or Path.cwd()
|
|
79
|
+
|
|
80
|
+
return project_root / "ref-reports" / "handoff_log.jsonl"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def log_event(
|
|
84
|
+
event_type: EventType,
|
|
85
|
+
project_root: Path | None = None,
|
|
86
|
+
**kwargs: str | list[str] | None,
|
|
87
|
+
) -> bool:
|
|
88
|
+
"""Log a handoff event to handoff_log.jsonl.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
event_type: Type of event to log.
|
|
92
|
+
project_root: Project root directory.
|
|
93
|
+
**kwargs: Additional event-specific fields.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if logged successfully, False otherwise.
|
|
97
|
+
"""
|
|
98
|
+
log_path = _get_log_path(project_root)
|
|
99
|
+
|
|
100
|
+
# Ensure directory exists
|
|
101
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
|
|
103
|
+
user = get_current_user_id(project_root)
|
|
104
|
+
|
|
105
|
+
event = {
|
|
106
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
107
|
+
"event": event_type,
|
|
108
|
+
"user": user,
|
|
109
|
+
**{k: v for k, v in kwargs.items() if v is not None},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
with log_path.open("a", encoding="utf-8") as f:
|
|
114
|
+
f.write(json.dumps(event) + "\n")
|
|
115
|
+
logger.info("event_logged", event_type=event_type, user=user)
|
|
116
|
+
return True
|
|
117
|
+
except OSError as e:
|
|
118
|
+
logger.error("event_log_failed", error=str(e))
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def log_agent_activation(agent: str, project_root: Path | None = None) -> bool:
|
|
123
|
+
"""Log agent activation event and create context graph node.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
agent: Agent that was activated.
|
|
127
|
+
project_root: Project root directory.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
True if logged successfully.
|
|
131
|
+
"""
|
|
132
|
+
# Log to handoff_log.jsonl (existing behavior)
|
|
133
|
+
result = log_event("agent_activation", project_root, agent=agent)
|
|
134
|
+
|
|
135
|
+
# Create agent_session node in context graph and link to active story.
|
|
136
|
+
try:
|
|
137
|
+
from ref_agents.tools.context_graph import ContextNode, get_context_graph
|
|
138
|
+
from ref_agents.workflow.state_machine import WorkflowManager
|
|
139
|
+
|
|
140
|
+
graph = get_context_graph(project_root)
|
|
141
|
+
ts = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
|
|
142
|
+
node_id = f"agent_session-{ts}"
|
|
143
|
+
|
|
144
|
+
# Resolve active story for story_id tagging and edge creation.
|
|
145
|
+
active_story_id: str | None = None
|
|
146
|
+
try:
|
|
147
|
+
active_story_id = WorkflowManager().get_active_story()
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
node = ContextNode(
|
|
152
|
+
id=node_id,
|
|
153
|
+
node_type="agent_session",
|
|
154
|
+
content=f"Agent session: {agent}",
|
|
155
|
+
story_id=active_story_id or "",
|
|
156
|
+
metadata={"agent": agent},
|
|
157
|
+
)
|
|
158
|
+
graph.add_node(node)
|
|
159
|
+
|
|
160
|
+
# Link to story node if one is active and exists in graph.
|
|
161
|
+
if active_story_id:
|
|
162
|
+
story_node_candidates = [
|
|
163
|
+
n
|
|
164
|
+
for n in graph._graph.nodes
|
|
165
|
+
if graph._graph.nodes[n].get("node_type") == "story"
|
|
166
|
+
and active_story_id in n
|
|
167
|
+
]
|
|
168
|
+
for story_node_id in story_node_candidates:
|
|
169
|
+
try:
|
|
170
|
+
graph.add_edge(node_id, story_node_id, "affects")
|
|
171
|
+
except ValueError:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
graph.save()
|
|
175
|
+
logger.info(
|
|
176
|
+
"agent_session_node_created",
|
|
177
|
+
node_id=node_id,
|
|
178
|
+
agent=agent,
|
|
179
|
+
story_id=active_story_id,
|
|
180
|
+
)
|
|
181
|
+
except (OSError, ValueError, ImportError) as e:
|
|
182
|
+
# Non-blocking: log error but don't fail activation
|
|
183
|
+
logger.warning("context_graph_node_failed", agent=agent, error=str(e))
|
|
184
|
+
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def log_ownership_transfer( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
|
|
189
|
+
story_id: str,
|
|
190
|
+
from_owner: str,
|
|
191
|
+
to_owner: str,
|
|
192
|
+
reason: str,
|
|
193
|
+
project_root: Path | None = None,
|
|
194
|
+
) -> bool:
|
|
195
|
+
"""Log ownership transfer event.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
story_id: Story being transferred.
|
|
199
|
+
from_owner: Previous owner.
|
|
200
|
+
to_owner: New owner.
|
|
201
|
+
reason: Reason for transfer.
|
|
202
|
+
project_root: Project root directory.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
True if logged successfully.
|
|
206
|
+
"""
|
|
207
|
+
result = log_event(
|
|
208
|
+
"ownership_transfer",
|
|
209
|
+
project_root,
|
|
210
|
+
story_id=story_id,
|
|
211
|
+
from_owner=from_owner,
|
|
212
|
+
to_owner=to_owner,
|
|
213
|
+
reason=reason,
|
|
214
|
+
)
|
|
215
|
+
_write_graph_node(
|
|
216
|
+
"agent_session",
|
|
217
|
+
f"Ownership: {from_owner} → {to_owner}",
|
|
218
|
+
story_id,
|
|
219
|
+
{},
|
|
220
|
+
)
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def log_escalation( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
|
|
225
|
+
level: str,
|
|
226
|
+
from_agent: str,
|
|
227
|
+
to_agents: list[str],
|
|
228
|
+
reason: str,
|
|
229
|
+
story_id: str | None = None,
|
|
230
|
+
project_root: Path | None = None,
|
|
231
|
+
) -> bool:
|
|
232
|
+
"""Log escalation event.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
level: Escalation level (L1, L2, L3).
|
|
236
|
+
from_agent: Agent that escalated.
|
|
237
|
+
to_agents: Agents escalated to.
|
|
238
|
+
reason: Reason for escalation.
|
|
239
|
+
story_id: Related story ID.
|
|
240
|
+
project_root: Project root directory.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
True if logged successfully.
|
|
244
|
+
"""
|
|
245
|
+
result = log_event(
|
|
246
|
+
"escalation",
|
|
247
|
+
project_root,
|
|
248
|
+
level=level,
|
|
249
|
+
from_agent=from_agent,
|
|
250
|
+
to_agents=to_agents,
|
|
251
|
+
reason=reason,
|
|
252
|
+
story_id=story_id,
|
|
253
|
+
)
|
|
254
|
+
_write_graph_node(
|
|
255
|
+
"issue",
|
|
256
|
+
f"Escalation {level}: {reason[:100]}",
|
|
257
|
+
story_id,
|
|
258
|
+
{"level": level},
|
|
259
|
+
)
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def log_debt_found( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
|
|
264
|
+
debt_id: str,
|
|
265
|
+
location: str,
|
|
266
|
+
debt_type: str,
|
|
267
|
+
severity: str,
|
|
268
|
+
story_id: str | None = None,
|
|
269
|
+
project_root: Path | None = None,
|
|
270
|
+
) -> bool:
|
|
271
|
+
"""Log technical debt discovery event.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
debt_id: Debt registry ID.
|
|
275
|
+
location: File and line location.
|
|
276
|
+
debt_type: Type of debt.
|
|
277
|
+
severity: Debt severity.
|
|
278
|
+
story_id: Story during which debt was found.
|
|
279
|
+
project_root: Project root directory.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
True if logged successfully.
|
|
283
|
+
"""
|
|
284
|
+
result = log_event(
|
|
285
|
+
"debt_logged",
|
|
286
|
+
project_root,
|
|
287
|
+
debt_id=debt_id,
|
|
288
|
+
location=location,
|
|
289
|
+
debt_type=debt_type,
|
|
290
|
+
severity=severity,
|
|
291
|
+
story_id=story_id,
|
|
292
|
+
)
|
|
293
|
+
_write_graph_node(
|
|
294
|
+
"issue",
|
|
295
|
+
f"Debt [{severity}] {debt_type} at {location}",
|
|
296
|
+
story_id,
|
|
297
|
+
{"debt_id": debt_id},
|
|
298
|
+
)
|
|
299
|
+
return result
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def log_phase_complete( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
|
|
303
|
+
story_id: str,
|
|
304
|
+
phase: str,
|
|
305
|
+
outcome: str,
|
|
306
|
+
next_phase: str | None = None,
|
|
307
|
+
project_root: Path | None = None,
|
|
308
|
+
) -> bool:
|
|
309
|
+
"""Log phase completion event.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
story_id: Story that completed phase.
|
|
313
|
+
phase: Phase that was completed.
|
|
314
|
+
outcome: Outcome (passed, failed).
|
|
315
|
+
next_phase: Next phase in flow.
|
|
316
|
+
project_root: Project root directory.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
True if logged successfully.
|
|
320
|
+
"""
|
|
321
|
+
result = log_event(
|
|
322
|
+
"phase_complete",
|
|
323
|
+
project_root,
|
|
324
|
+
story_id=story_id,
|
|
325
|
+
phase=phase,
|
|
326
|
+
outcome=outcome,
|
|
327
|
+
next_phase=next_phase,
|
|
328
|
+
)
|
|
329
|
+
_write_graph_node(
|
|
330
|
+
"decision",
|
|
331
|
+
f"[{outcome.upper()}] {phase} → {next_phase or 'end'}",
|
|
332
|
+
story_id,
|
|
333
|
+
{},
|
|
334
|
+
)
|
|
335
|
+
return result
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def log_replan_triggered(
|
|
339
|
+
story_id: str,
|
|
340
|
+
trigger: str,
|
|
341
|
+
impact: str,
|
|
342
|
+
project_root: Path | None = None,
|
|
343
|
+
) -> bool:
|
|
344
|
+
"""Log replan trigger event.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
story_id: Story being replanned.
|
|
348
|
+
trigger: What triggered the replan (new req, blocker, etc).
|
|
349
|
+
impact: Expected impact (scope change, delay).
|
|
350
|
+
project_root: Project root directory.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
True if logged successfully.
|
|
354
|
+
"""
|
|
355
|
+
result = log_event(
|
|
356
|
+
"replan_triggered",
|
|
357
|
+
project_root,
|
|
358
|
+
story_id=story_id,
|
|
359
|
+
trigger=trigger,
|
|
360
|
+
impact=impact,
|
|
361
|
+
)
|
|
362
|
+
_write_graph_node(
|
|
363
|
+
"issue",
|
|
364
|
+
f"Replan: {trigger[:100]}",
|
|
365
|
+
story_id,
|
|
366
|
+
{"impact": impact},
|
|
367
|
+
)
|
|
368
|
+
return result
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Ignore pattern matcher for .gitignore and .dockerignore files.
|
|
2
|
+
|
|
3
|
+
Parses ignore files and checks if paths match ignore patterns.
|
|
4
|
+
Used to avoid flagging intentionally ignored files as dead code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import structlog
|
|
12
|
+
|
|
13
|
+
logger = structlog.get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class IgnorePattern:
|
|
18
|
+
"""Represents a single ignore pattern.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
pattern: Original pattern string.
|
|
22
|
+
regex: Compiled regex for matching.
|
|
23
|
+
negated: True if pattern starts with ! (negation).
|
|
24
|
+
directory_only: True if pattern ends with / (directory only).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
pattern: str
|
|
28
|
+
regex: re.Pattern[str]
|
|
29
|
+
negated: bool = False
|
|
30
|
+
directory_only: bool = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class IgnorePatternMatcher:
|
|
35
|
+
"""Matches file paths against .gitignore/.dockerignore patterns.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
patterns: List of parsed ignore patterns.
|
|
39
|
+
base_dir: Base directory for relative path resolution.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
patterns: list[IgnorePattern] = field(default_factory=list)
|
|
43
|
+
base_dir: Path = field(default_factory=Path.cwd)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_file(
|
|
47
|
+
cls,
|
|
48
|
+
ignore_file: Path,
|
|
49
|
+
base_dir: Path | None = None,
|
|
50
|
+
) -> "IgnorePatternMatcher":
|
|
51
|
+
"""Create matcher from ignore file.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
ignore_file: Path to .gitignore or .dockerignore file.
|
|
55
|
+
base_dir: Base directory for pattern matching.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Configured IgnorePatternMatcher instance.
|
|
59
|
+
"""
|
|
60
|
+
matcher = cls(base_dir=base_dir or ignore_file.parent)
|
|
61
|
+
|
|
62
|
+
if not ignore_file.exists():
|
|
63
|
+
return matcher
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
content = ignore_file.read_text(encoding="utf-8")
|
|
67
|
+
for line in content.splitlines():
|
|
68
|
+
pattern = matcher._parse_line(line)
|
|
69
|
+
if pattern:
|
|
70
|
+
matcher.patterns.append(pattern)
|
|
71
|
+
except OSError as e:
|
|
72
|
+
logger.warning(
|
|
73
|
+
"ignore_file_read_error", file=str(ignore_file), error=str(e)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return matcher
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_directory(cls, directory: Path) -> "IgnorePatternMatcher":
|
|
80
|
+
"""Create matcher from all ignore files in directory.
|
|
81
|
+
|
|
82
|
+
Loads .gitignore and .dockerignore if present.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
directory: Directory to scan for ignore files.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Combined IgnorePatternMatcher instance.
|
|
89
|
+
"""
|
|
90
|
+
matcher = cls(base_dir=directory)
|
|
91
|
+
|
|
92
|
+
for ignore_name in [".gitignore", ".dockerignore"]:
|
|
93
|
+
ignore_file = directory / ignore_name
|
|
94
|
+
if ignore_file.exists():
|
|
95
|
+
file_matcher = cls.from_file(ignore_file, directory)
|
|
96
|
+
matcher.patterns.extend(file_matcher.patterns)
|
|
97
|
+
logger.debug(
|
|
98
|
+
"loaded_ignore_file",
|
|
99
|
+
file=ignore_name,
|
|
100
|
+
patterns=len(file_matcher.patterns),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return matcher
|
|
104
|
+
|
|
105
|
+
def _parse_line(self, line: str) -> IgnorePattern | None:
|
|
106
|
+
"""Parse a single ignore pattern line.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
line: Line from ignore file.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Parsed IgnorePattern or None if line is empty/comment.
|
|
113
|
+
"""
|
|
114
|
+
# Strip whitespace
|
|
115
|
+
line = line.strip()
|
|
116
|
+
|
|
117
|
+
# Skip empty lines and comments
|
|
118
|
+
if not line or line.startswith("#"):
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
negated = False
|
|
122
|
+
directory_only = False
|
|
123
|
+
|
|
124
|
+
# Check for negation
|
|
125
|
+
if line.startswith("!"):
|
|
126
|
+
negated = True
|
|
127
|
+
line = line[1:]
|
|
128
|
+
|
|
129
|
+
# Check for directory-only pattern
|
|
130
|
+
if line.endswith("/"):
|
|
131
|
+
directory_only = True
|
|
132
|
+
line = line[:-1]
|
|
133
|
+
|
|
134
|
+
# Convert gitignore pattern to regex
|
|
135
|
+
regex = self._pattern_to_regex(line)
|
|
136
|
+
|
|
137
|
+
return IgnorePattern(
|
|
138
|
+
pattern=line,
|
|
139
|
+
regex=regex,
|
|
140
|
+
negated=negated,
|
|
141
|
+
directory_only=directory_only,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _pattern_to_regex(self, pattern: str) -> re.Pattern[str]:
|
|
145
|
+
"""Convert gitignore pattern to compiled regex.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
pattern: Gitignore-style pattern.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Compiled regex pattern.
|
|
152
|
+
"""
|
|
153
|
+
# Handle leading slash (anchor to root)
|
|
154
|
+
anchored = pattern.startswith("/")
|
|
155
|
+
if anchored:
|
|
156
|
+
pattern = pattern[1:]
|
|
157
|
+
|
|
158
|
+
# Escape special regex characters except * and ?
|
|
159
|
+
escaped = ""
|
|
160
|
+
i = 0
|
|
161
|
+
while i < len(pattern):
|
|
162
|
+
char = pattern[i]
|
|
163
|
+
if char == "*":
|
|
164
|
+
if i + 1 < len(pattern) and pattern[i + 1] == "*":
|
|
165
|
+
# ** matches any path including /
|
|
166
|
+
escaped += ".*"
|
|
167
|
+
i += 2
|
|
168
|
+
# Skip following /
|
|
169
|
+
if i < len(pattern) and pattern[i] == "/":
|
|
170
|
+
i += 1
|
|
171
|
+
continue
|
|
172
|
+
else:
|
|
173
|
+
# * matches anything except /
|
|
174
|
+
escaped += "[^/]*"
|
|
175
|
+
elif char == "?":
|
|
176
|
+
escaped += "[^/]"
|
|
177
|
+
elif char in ".^$+{}[]|()":
|
|
178
|
+
escaped += "\\" + char
|
|
179
|
+
else:
|
|
180
|
+
escaped += char
|
|
181
|
+
i += 1
|
|
182
|
+
|
|
183
|
+
# Build final regex
|
|
184
|
+
if anchored:
|
|
185
|
+
regex_str = f"^{escaped}"
|
|
186
|
+
else:
|
|
187
|
+
# Match anywhere in path
|
|
188
|
+
regex_str = f"(^|/){escaped}"
|
|
189
|
+
|
|
190
|
+
# Match end of string or path separator
|
|
191
|
+
regex_str += "(/|$)"
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
return re.compile(regex_str)
|
|
195
|
+
except re.error as e:
|
|
196
|
+
logger.warning("invalid_ignore_pattern", pattern=pattern, error=str(e))
|
|
197
|
+
# Return pattern that matches nothing
|
|
198
|
+
return re.compile(r"^\b$")
|
|
199
|
+
|
|
200
|
+
def is_ignored(self, path: Path | str, is_dir: bool = False) -> bool:
|
|
201
|
+
"""Check if path matches any ignore pattern.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
path: File or directory path (relative to base_dir).
|
|
205
|
+
is_dir: True if path is a directory.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if path should be ignored.
|
|
209
|
+
"""
|
|
210
|
+
if isinstance(path, str):
|
|
211
|
+
path = Path(path)
|
|
212
|
+
|
|
213
|
+
# Make path relative to base_dir
|
|
214
|
+
try:
|
|
215
|
+
rel_path = path.relative_to(self.base_dir)
|
|
216
|
+
except ValueError:
|
|
217
|
+
rel_path = path
|
|
218
|
+
|
|
219
|
+
# Convert to forward slashes for matching
|
|
220
|
+
path_str = str(rel_path).replace("\\", "/")
|
|
221
|
+
|
|
222
|
+
ignored = False
|
|
223
|
+
|
|
224
|
+
for pattern in self.patterns:
|
|
225
|
+
# For directory-only patterns, check if any parent matches
|
|
226
|
+
if pattern.directory_only:
|
|
227
|
+
if is_dir:
|
|
228
|
+
if pattern.regex.search(path_str):
|
|
229
|
+
ignored = not pattern.negated
|
|
230
|
+
else:
|
|
231
|
+
# Check if file is under a matching directory (not the file itself)
|
|
232
|
+
# Only check parent directories, not the file's own name
|
|
233
|
+
if len(rel_path.parts) > 1:
|
|
234
|
+
for part_idx in range(len(rel_path.parts) - 1):
|
|
235
|
+
parent_path = "/".join(rel_path.parts[: part_idx + 1])
|
|
236
|
+
if pattern.regex.search(parent_path):
|
|
237
|
+
ignored = not pattern.negated
|
|
238
|
+
break
|
|
239
|
+
elif pattern.regex.search(path_str):
|
|
240
|
+
# Regular pattern
|
|
241
|
+
ignored = not pattern.negated
|
|
242
|
+
|
|
243
|
+
return ignored
|
|
244
|
+
|
|
245
|
+
def filter_paths(self, paths: list[Path]) -> list[Path]:
|
|
246
|
+
"""Filter list of paths, removing ignored ones.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
paths: List of paths to filter.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Paths that are NOT ignored.
|
|
253
|
+
"""
|
|
254
|
+
return [p for p in paths if not self.is_ignored(p, is_dir=p.is_dir())]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def is_intentionally_ignored(file_path: Path, project_root: Path) -> bool:
|
|
258
|
+
"""Check if file is in .gitignore or .dockerignore.
|
|
259
|
+
|
|
260
|
+
Convenience function for dead code detection.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
file_path: Path to check.
|
|
264
|
+
project_root: Project root directory.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
True if file is intentionally ignored.
|
|
268
|
+
"""
|
|
269
|
+
matcher = IgnorePatternMatcher.from_directory(project_root)
|
|
270
|
+
return matcher.is_ignored(file_path)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Workflow State Machine for REF Agents.
|
|
2
|
+
|
|
3
|
+
This package provides SDLC workflow tracking:
|
|
4
|
+
- State persistence per story
|
|
5
|
+
- Phase transitions with artifact validation
|
|
6
|
+
- Parallel task coordination
|
|
7
|
+
- Capability detection for tools
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ref_agents.workflow.capabilities import CapabilityDetector
|
|
11
|
+
from ref_agents.workflow.state_machine import WorkflowManager, WorkflowState
|
|
12
|
+
from ref_agents.workflow.transitions import TransitionValidator
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"CapabilityDetector",
|
|
16
|
+
"TransitionValidator",
|
|
17
|
+
"WorkflowManager",
|
|
18
|
+
"WorkflowState",
|
|
19
|
+
]
|