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,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
+ ]