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,247 @@
1
+ """REF license client — validates API keys against ref-license backend.
2
+
3
+ Handles:
4
+ - Key validation via POST /validate (with in-memory + file cache)
5
+ - Paid asset fetching: GET /prompt/:name, /rule/:path, /config/:path
6
+ - Graceful degrade: network error → cached status → free tier (never hard-fails)
7
+
8
+ Environment:
9
+ REF_API_KEY Single API key (sk_ref_...)
10
+ REF_LICENSE_URL Override default server URL (optional)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import time
18
+ import urllib.error
19
+ import urllib.request
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+ from typing import Literal
23
+
24
+ import structlog
25
+
26
+ logger = structlog.get_logger(__name__)
27
+
28
+ # ── Constants ────────────────────────────────────────────────────────────────
29
+
30
+ REF_LICENSE_URL: str = os.environ.get(
31
+ "REF_LICENSE_URL", "https://ref-license.workers.dev"
32
+ ).rstrip("/")
33
+
34
+ CACHE_TTL_SECONDS: int = 14_400 # 4 hrs — revalidate on next boot after this
35
+ OFFLINE_GRACE_SECONDS: int = (
36
+ 86_400 # 24 hrs — serve cached status if server unreachable
37
+ )
38
+ REQUEST_TIMEOUT_SECONDS: int = 5
39
+
40
+ _CACHE_FILE = Path.home() / ".ref_cache" / "license.json"
41
+
42
+ Plan = Literal["free", "paid"]
43
+
44
+
45
+ # ── Data classes ─────────────────────────────────────────────────────────────
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class LicenseStatus:
50
+ """Result of a license validation check."""
51
+
52
+ valid: bool
53
+ plan: Plan
54
+ reason: str = ""
55
+
56
+ @property
57
+ def is_paid(self) -> bool:
58
+ """Return True if the active plan is paid."""
59
+ return self.valid and self.plan == "paid"
60
+
61
+
62
+ _FREE = LicenseStatus(valid=False, plan="free", reason="no_key")
63
+ _FREE_DEGRADED = LicenseStatus(valid=False, plan="free", reason="server_unreachable")
64
+
65
+
66
+ # ── In-process cache ─────────────────────────────────────────────────────────
67
+
68
+ _mem_status: LicenseStatus | None = None
69
+ _mem_fetched_at: float = 0.0
70
+
71
+
72
+ def _load_file_cache() -> tuple[LicenseStatus, float] | None:
73
+ """Read license status from disk cache.
74
+
75
+ Returns:
76
+ (status, fetched_at) tuple, or None if cache missing/corrupt.
77
+ """
78
+ try:
79
+ if not _CACHE_FILE.exists():
80
+ return None
81
+ data = json.loads(_CACHE_FILE.read_text())
82
+ status = LicenseStatus(
83
+ valid=bool(data["valid"]),
84
+ plan=data["plan"],
85
+ reason=data.get("reason", ""),
86
+ )
87
+ return status, float(data["fetched_at"])
88
+ except (KeyError, json.JSONDecodeError, OSError):
89
+ return None
90
+
91
+
92
+ def _write_file_cache(status: LicenseStatus) -> None:
93
+ """Persist license status to disk for offline-grace support."""
94
+ try:
95
+ _CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
96
+ _CACHE_FILE.write_text(
97
+ json.dumps(
98
+ {
99
+ "valid": status.valid,
100
+ "plan": status.plan,
101
+ "reason": status.reason,
102
+ "fetched_at": time.time(),
103
+ }
104
+ )
105
+ )
106
+ except OSError as e:
107
+ logger.warning("license_cache_write_failed", error=str(e))
108
+
109
+
110
+ # ── Network validation ────────────────────────────────────────────────────────
111
+
112
+
113
+ def _fetch_from_server(api_key: str) -> LicenseStatus | None:
114
+ """Call POST /validate. Returns None on any network/parse error.
115
+
116
+ Args:
117
+ api_key: The REF_API_KEY value.
118
+
119
+ Returns:
120
+ LicenseStatus on success, None on failure.
121
+ """
122
+ url = f"{REF_LICENSE_URL}/validate"
123
+ payload = json.dumps({"key": api_key}).encode()
124
+ req = urllib.request.Request(
125
+ url,
126
+ data=payload,
127
+ headers={
128
+ "Content-Type": "application/json",
129
+ "Authorization": f"Bearer {api_key}",
130
+ },
131
+ method="POST",
132
+ )
133
+ try:
134
+ with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT_SECONDS) as resp:
135
+ body = json.loads(resp.read())
136
+ valid = bool(body.get("valid", False))
137
+ plan: Plan = "paid" if body.get("plan") == "paid" else "free"
138
+ reason: str = body.get("reason", "")
139
+ return LicenseStatus(valid=valid, plan=plan, reason=reason)
140
+ except urllib.error.HTTPError as e:
141
+ logger.warning("license_validate_http_error", status=e.code)
142
+ return None
143
+ except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError) as e:
144
+ logger.warning("license_validate_network_error", error=str(e))
145
+ return None
146
+
147
+
148
+ # ── Public API ────────────────────────────────────────────────────────────────
149
+
150
+
151
+ def get_status() -> LicenseStatus:
152
+ """Return current license status.
153
+
154
+ Resolution order:
155
+ 1. In-memory cache (TTL=4hrs)
156
+ 2. Network validate → update both caches
157
+ 3. File cache within offline grace period (24hrs)
158
+ 4. Free tier (never hard-fails)
159
+
160
+ Returns:
161
+ LicenseStatus with valid/plan fields.
162
+ """
163
+ global _mem_status, _mem_fetched_at
164
+
165
+ api_key = os.environ.get("REF_API_KEY", "").strip()
166
+ if not api_key:
167
+ return _FREE
168
+
169
+ now = time.time()
170
+
171
+ # 1. In-memory cache still fresh
172
+ if _mem_status is not None and (now - _mem_fetched_at) < CACHE_TTL_SECONDS:
173
+ return _mem_status
174
+
175
+ # 2. Fetch from server
176
+ fetched = _fetch_from_server(api_key)
177
+ if fetched is not None:
178
+ _mem_status = fetched
179
+ _mem_fetched_at = now
180
+ _write_file_cache(fetched)
181
+ logger.info("license_validated", plan=fetched.plan, valid=fetched.valid)
182
+ return fetched
183
+
184
+ # 3. File cache within offline grace
185
+ cached = _load_file_cache()
186
+ if cached is not None:
187
+ status, fetched_at = cached
188
+ if (now - fetched_at) < OFFLINE_GRACE_SECONDS:
189
+ logger.warning(
190
+ "license_using_cached_offline", age_hours=(now - fetched_at) / 3600
191
+ )
192
+ _mem_status = status
193
+ _mem_fetched_at = now # don't hammer server on every call
194
+ return status
195
+
196
+ # 4. Degrade to free — never hard-fail
197
+ logger.warning("license_degraded_to_free", reason="no_valid_cache")
198
+ return _FREE_DEGRADED
199
+
200
+
201
+ def fetch_asset(
202
+ asset_type: Literal["prompt", "rule", "config"], path: str
203
+ ) -> str | None:
204
+ """Fetch a paid asset from the license server.
205
+
206
+ Args:
207
+ asset_type: One of "prompt", "rule", "config".
208
+ path: Asset path (e.g. "developer", "architecture/system_design", "languages/python").
209
+
210
+ Returns:
211
+ Asset text content, or None on error/not found.
212
+ """
213
+ api_key = os.environ.get("REF_API_KEY", "").strip()
214
+ if not api_key:
215
+ return None
216
+
217
+ url = f"{REF_LICENSE_URL}/{asset_type}/{path}"
218
+ req = urllib.request.Request(
219
+ url,
220
+ headers={"Authorization": f"Bearer {api_key}"},
221
+ method="GET",
222
+ )
223
+ try:
224
+ with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT_SECONDS) as resp:
225
+ return resp.read().decode("utf-8")
226
+ except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError) as e:
227
+ logger.warning(
228
+ "asset_fetch_failed", asset_type=asset_type, path=path, error=str(e)
229
+ )
230
+ return None
231
+
232
+
233
+ def upgrade_message(feature: str = "agent prompts") -> str:
234
+ """Return a clear upgrade prompt for free-tier users.
235
+
236
+ Args:
237
+ feature: Name of the gated feature.
238
+
239
+ Returns:
240
+ Human-readable upgrade message.
241
+ """
242
+ return (
243
+ f"⚠️ **REF Pro required** — {feature} require an active REF subscription.\n\n"
244
+ "Subscribe at **https://ref.dev** then set:\n"
245
+ "```\nexport REF_API_KEY=sk_ref_...\n```\n"
246
+ "Free tier: all scanners (`scan`, `log`, `workflow`, `validate`) remain available."
247
+ )
@@ -0,0 +1,22 @@
1
+ """REF-Agents data models.
2
+
3
+ Provides structured data models for requirements, INVEST scoring, Gherkin,
4
+ hierarchy, and version management.
5
+ """
6
+
7
+ from ref_agents.models.gherkin import GherkinScenario, GherkinStep
8
+ from ref_agents.models.hierarchy import Epic, Feature, Story
9
+ from ref_agents.models.invest import INVESTScores, INVESTPrinciple
10
+ from ref_agents.models.version import RequirementVersion, VersionStatus
11
+
12
+ __all__ = [
13
+ "GherkinStep",
14
+ "GherkinScenario",
15
+ "INVESTPrinciple",
16
+ "INVESTScores",
17
+ "Epic",
18
+ "Feature",
19
+ "Story",
20
+ "RequirementVersion",
21
+ "VersionStatus",
22
+ ]
@@ -0,0 +1,45 @@
1
+ """Gherkin/BDD format models for structured acceptance criteria."""
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class GherkinStep(BaseModel):
9
+ """Single Gherkin step (Given/When/Then/And/But)."""
10
+
11
+ keyword: Literal["Given", "When", "Then", "And", "But"] = Field(
12
+ description="Gherkin keyword"
13
+ )
14
+ text: str = Field(description="Step text content")
15
+
16
+ def to_markdown(self) -> str:
17
+ """Convert step to markdown format."""
18
+ return f"**{self.keyword}** {self.text}"
19
+
20
+ def to_gherkin(self) -> str:
21
+ """Convert step to Gherkin text format."""
22
+ return f"{self.keyword} {self.text}"
23
+
24
+
25
+ class GherkinScenario(BaseModel):
26
+ """Gherkin scenario with multiple steps."""
27
+
28
+ scenario: str = Field(description="Scenario title/description")
29
+ steps: list[GherkinStep] = Field(
30
+ default_factory=list, description="Ordered list of Gherkin steps"
31
+ )
32
+
33
+ def to_markdown(self) -> str:
34
+ """Convert scenario to markdown format."""
35
+ lines = [f"### {self.scenario}", ""]
36
+ for step in self.steps:
37
+ lines.append(step.to_markdown())
38
+ return "\n".join(lines)
39
+
40
+ def to_gherkin(self) -> str:
41
+ """Convert scenario to Gherkin text format."""
42
+ lines = [f"Scenario: {self.scenario}", ""]
43
+ for step in self.steps:
44
+ lines.append(step.to_gherkin())
45
+ return "\n".join(lines)
@@ -0,0 +1,80 @@
1
+ """Hierarchy models for Epic → Feature → Story structure."""
2
+
3
+ from typing import Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class Epic(BaseModel):
9
+ """Epic model (top-level requirement grouping)."""
10
+
11
+ epic_id: str = Field(description="Unique epic identifier (e.g., EPIC-001)")
12
+ title: str = Field(description="Epic title")
13
+ description: str = Field(default="", description="Epic description")
14
+ status: str = Field(default="Draft", description="Epic status")
15
+
16
+ def to_dict(self) -> dict:
17
+ """Convert epic to dictionary."""
18
+ return {
19
+ "epic_id": self.epic_id,
20
+ "title": self.title,
21
+ "description": self.description,
22
+ "status": self.status,
23
+ }
24
+
25
+
26
+ class Feature(BaseModel):
27
+ """Feature model (mid-level requirement grouping under Epic)."""
28
+
29
+ feature_id: str = Field(description="Unique feature identifier (e.g., FEAT-001)")
30
+ epic_id: str = Field(description="Parent epic identifier")
31
+ title: str = Field(description="Feature title")
32
+ description: str = Field(default="", description="Feature description")
33
+ status: str = Field(default="Draft", description="Feature status")
34
+
35
+ def to_dict(self) -> dict:
36
+ """Convert feature to dictionary."""
37
+ return {
38
+ "feature_id": self.feature_id,
39
+ "epic_id": self.epic_id,
40
+ "title": self.title,
41
+ "description": self.description,
42
+ "status": self.status,
43
+ }
44
+
45
+
46
+ class Story(BaseModel):
47
+ """Story model (low-level requirement, can be flat or hierarchical)."""
48
+
49
+ story_id: str = Field(description="Unique story identifier (e.g., STORY-001)")
50
+ title: str = Field(description="Story title")
51
+ description: str = Field(default="", description="Story description")
52
+ user_story: str = Field(
53
+ default="", description="User story format: As a... I want... So that..."
54
+ )
55
+ epic_id: Optional[str] = Field(
56
+ default=None, description="Parent epic identifier (optional for flat stories)"
57
+ )
58
+ feature_id: Optional[str] = Field(
59
+ default=None,
60
+ description="Parent feature identifier (optional for flat stories)",
61
+ )
62
+ status: str = Field(default="Draft", description="Story status")
63
+ priority: str = Field(default="P2", description="Story priority (P1/P2/P3)")
64
+
65
+ def is_flat(self) -> bool:
66
+ """Check if story is flat (no epic/feature parent)."""
67
+ return self.epic_id is None and self.feature_id is None
68
+
69
+ def to_dict(self) -> dict:
70
+ """Convert story to dictionary."""
71
+ return {
72
+ "story_id": self.story_id,
73
+ "title": self.title,
74
+ "description": self.description,
75
+ "user_story": self.user_story,
76
+ "epic_id": self.epic_id,
77
+ "feature_id": self.feature_id,
78
+ "status": self.status,
79
+ "priority": self.priority,
80
+ }
@@ -0,0 +1,59 @@
1
+ """INVEST scoring models for user story quality assessment.
2
+
3
+ INVEST principles: Independent, Negotiable, Valuable, Estimable, Small, Testable.
4
+ """
5
+
6
+ from enum import Enum
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class INVESTPrinciple(str, Enum):
12
+ """INVEST principle enumeration."""
13
+
14
+ INDEPENDENT = "Independent"
15
+ NEGOTIABLE = "Negotiable"
16
+ VALUABLE = "Valuable"
17
+ ESTIMABLE = "Estimable"
18
+ SMALL = "Small"
19
+ TESTABLE = "Testable"
20
+
21
+
22
+ class INVESTScores(BaseModel):
23
+ """INVEST scores for a user story.
24
+
25
+ Each principle scored 0-5:
26
+ - 0: Not met
27
+ - 1-2: Poor
28
+ - 3: Acceptable
29
+ - 4-5: Excellent
30
+ """
31
+
32
+ independent: int = Field(ge=0, le=5, description="Independent score (0-5)")
33
+ negotiable: int = Field(ge=0, le=5, description="Negotiable score (0-5)")
34
+ valuable: int = Field(ge=0, le=5, description="Valuable score (0-5)")
35
+ estimable: int = Field(ge=0, le=5, description="Estimable score (0-5)")
36
+ small: int = Field(ge=0, le=5, description="Small score (0-5)")
37
+ testable: int = Field(ge=0, le=5, description="Testable score (0-5)")
38
+
39
+ def overall_score(self) -> float:
40
+ """Calculate overall INVEST score (average of all principles)."""
41
+ return (
42
+ self.independent
43
+ + self.negotiable
44
+ + self.valuable
45
+ + self.estimable
46
+ + self.small
47
+ + self.testable
48
+ ) / 6.0
49
+
50
+ def to_dict(self) -> dict[str, int]:
51
+ """Convert scores to dictionary."""
52
+ return {
53
+ "independent": self.independent,
54
+ "negotiable": self.negotiable,
55
+ "valuable": self.valuable,
56
+ "estimable": self.estimable,
57
+ "small": self.small,
58
+ "testable": self.testable,
59
+ }
@@ -0,0 +1,49 @@
1
+ """Version management models for requirement tracking."""
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Optional
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class VersionStatus(str, Enum):
11
+ """Requirement version status."""
12
+
13
+ CURRENT = "Current"
14
+ SUPERSEDED = "Superseded"
15
+ DRAFT = "Draft"
16
+
17
+
18
+ class RequirementVersion(BaseModel):
19
+ """Version metadata for a requirement artifact."""
20
+
21
+ artifact_id: str = Field(
22
+ description="Artifact identifier (STORY-XXX, EPIC-XXX, etc.)"
23
+ )
24
+ version: str = Field(description="Version identifier (semantic or git commit hash)")
25
+ status: VersionStatus = Field(
26
+ default=VersionStatus.CURRENT, description="Version status"
27
+ )
28
+ created_at: datetime = Field(
29
+ default_factory=datetime.now, description="Version creation timestamp"
30
+ )
31
+ modified_at: Optional[datetime] = Field(
32
+ default=None, description="Last modification timestamp"
33
+ )
34
+ git_commit: Optional[str] = Field(default=None, description="Git commit hash")
35
+ modified_by: Optional[str] = Field(
36
+ default=None, description="Last modifier (git user email)"
37
+ )
38
+
39
+ def to_dict(self) -> dict:
40
+ """Convert version to dictionary."""
41
+ return {
42
+ "artifact_id": self.artifact_id,
43
+ "version": self.version,
44
+ "status": self.status.value,
45
+ "created_at": self.created_at.isoformat(),
46
+ "modified_at": self.modified_at.isoformat() if self.modified_at else None,
47
+ "git_commit": self.git_commit,
48
+ "modified_by": self.modified_by,
49
+ }
@@ -0,0 +1,9 @@
1
+ """REF Agent Prompts Package.
2
+
3
+ Contains prompt generators for all REF agents: discovery, architect,
4
+ developer, tester, reviewer, and scrum master.
5
+
6
+ Usage:
7
+ from ref_agents.prompts import start_agent
8
+ prompt = start_agent.get_developer_prompt()
9
+ """