adelie-ai 0.2.5 → 0.2.7

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 (63) hide show
  1. package/LICENSE +21 -21
  2. package/adelie/.python-version +1 -1
  3. package/adelie/__init__.py +16 -16
  4. package/adelie/agents/__init__.py +1 -1
  5. package/adelie/agents/analyst_ai.py +247 -247
  6. package/adelie/agents/coder_ai.py +294 -294
  7. package/adelie/agents/coder_manager.py +349 -349
  8. package/adelie/agents/expert_ai.py +547 -470
  9. package/adelie/agents/inform_ai.py +138 -138
  10. package/adelie/agents/monitor_ai.py +251 -251
  11. package/adelie/agents/research_ai.py +224 -224
  12. package/adelie/agents/reviewer_ai.py +165 -165
  13. package/adelie/agents/runner_ai.py +597 -597
  14. package/adelie/agents/scanner_ai.py +380 -380
  15. package/adelie/agents/tester_ai.py +361 -361
  16. package/adelie/agents/writer_ai.py +328 -286
  17. package/adelie/browser_search.py +417 -417
  18. package/adelie/cli.py +2293 -2273
  19. package/adelie/command_loader.py +137 -137
  20. package/adelie/config.py +92 -92
  21. package/adelie/context_compactor.py +360 -360
  22. package/adelie/context_engine.py +445 -445
  23. package/adelie/env_strategy.py +523 -523
  24. package/adelie/feedback_queue.py +159 -159
  25. package/adelie/git_ops.py +170 -170
  26. package/adelie/hooks.py +236 -236
  27. package/adelie/i18n.py +122 -122
  28. package/adelie/integrations/__init__.py +1 -1
  29. package/adelie/integrations/telegram_bot.py +471 -471
  30. package/adelie/interactive.py +559 -559
  31. package/adelie/kb/__init__.py +1 -1
  32. package/adelie/kb/embedding_store.py +249 -249
  33. package/adelie/kb/retriever.py +313 -313
  34. package/adelie/llm_client.py +536 -536
  35. package/adelie/log_rotation.py +67 -67
  36. package/adelie/loop_detector.py +554 -554
  37. package/adelie/loop_manual.md +141 -141
  38. package/adelie/main.py +6 -6
  39. package/adelie/metrics.py +307 -307
  40. package/adelie/orchestrator.py +1514 -1514
  41. package/adelie/phases.py +305 -300
  42. package/adelie/plan_mode.py +245 -245
  43. package/adelie/process_supervisor.py +351 -351
  44. package/adelie/project_context.py +217 -217
  45. package/adelie/prompt_loader.py +175 -175
  46. package/adelie/prompts/coder.md +30 -30
  47. package/adelie/prompts/expert.md +104 -104
  48. package/adelie/prompts/reviewer.md +32 -32
  49. package/adelie/pyproject.toml +15 -15
  50. package/adelie/registry.py +88 -88
  51. package/adelie/rules_loader.py +112 -112
  52. package/adelie/sandbox.py +388 -388
  53. package/adelie/scheduler.py +265 -265
  54. package/adelie/skill_manager.py +422 -422
  55. package/adelie/spec_chunker.py +241 -241
  56. package/adelie/spec_loader.py +384 -384
  57. package/adelie/tool_registry.py +369 -369
  58. package/adelie/ui_logger.py +337 -337
  59. package/adelie/utils/dep_sync.py +193 -193
  60. package/adelie/utils/import_checker.py +279 -279
  61. package/adelie/web_search.py +245 -245
  62. package/bin/adelie.js +84 -84
  63. package/package.json +46 -46
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 kimhyunbin
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kimhyunbin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1 +1 @@
1
- 3.10
1
+ 3.10
@@ -1,16 +1,16 @@
1
- """Adelie — Self-communicating autonomous AI loop system."""
2
-
3
- from pathlib import Path as _Path
4
- import json as _json
5
-
6
- def _get_version() -> str:
7
- """Read version from package.json (single source of truth)."""
8
- try:
9
- pkg = _Path(__file__).resolve().parent.parent / "package.json"
10
- if pkg.exists():
11
- return _json.loads(pkg.read_text(encoding="utf-8")).get("version", "0.0.0")
12
- except Exception:
13
- pass
14
- return "0.0.0"
15
-
16
- __version__ = _get_version()
1
+ """Adelie — Self-communicating autonomous AI loop system."""
2
+
3
+ from pathlib import Path as _Path
4
+ import json as _json
5
+
6
+ def _get_version() -> str:
7
+ """Read version from package.json (single source of truth)."""
8
+ try:
9
+ pkg = _Path(__file__).resolve().parent.parent / "package.json"
10
+ if pkg.exists():
11
+ return _json.loads(pkg.read_text(encoding="utf-8")).get("version", "0.0.0")
12
+ except Exception:
13
+ pass
14
+ return "0.0.0"
15
+
16
+ __version__ = "0.2.7"
@@ -1 +1 @@
1
- """adelie/agents package."""
1
+ """adelie/agents package."""
@@ -1,247 +1,247 @@
1
- """
2
- adelie/agents/analyst_ai.py
3
-
4
- Analyst AI — strategic analysis for market fit, revenue, and growth.
5
-
6
- Uses LLM to analyze the entire KB and produce actionable reports:
7
- - Market analysis & competitive landscape
8
- - Revenue strategy & monetization optimization
9
- - Growth opportunities & feature prioritization
10
- - User feedback synthesis (when available)
11
-
12
- Reports saved to .adelie/analysis/
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import json
18
- import re
19
- from datetime import datetime
20
- from pathlib import Path
21
-
22
- from rich.console import Console
23
-
24
- from adelie.config import WORKSPACE_PATH
25
- from adelie.kb import retriever
26
- from adelie.llm_client import generate
27
-
28
- console = Console()
29
-
30
- ANALYSIS_ROOT = WORKSPACE_PATH.parent / "analysis"
31
-
32
- SYSTEM_PROMPT = """You are Analyst AI — a business strategist and market analyst in an autonomous AI loop.
33
-
34
- You receive the project's entire Knowledge Base and must produce strategic analysis.
35
-
36
- Output a single valid JSON object:
37
- {
38
- "market_analysis": {
39
- "target_market": "Description of target market and users",
40
- "competitors": ["competitor1", "competitor2"],
41
- "differentiators": ["unique value prop 1", "unique value prop 2"],
42
- "market_size_estimate": "small/medium/large with reasoning"
43
- },
44
- "revenue_strategy": {
45
- "model": "SaaS subscription / freemium / one-time / etc",
46
- "pricing_tiers": [
47
- {"name": "Free", "price": "$0", "features": ["feature1"]},
48
- {"name": "Pro", "price": "$X/mo", "features": ["feature1", "feature2"]}
49
- ],
50
- "revenue_timeline": "When to expect first revenue"
51
- },
52
- "growth_opportunities": [
53
- {
54
- "opportunity": "Description",
55
- "effort": "low/medium/high",
56
- "impact": "low/medium/high",
57
- "priority": 1
58
- }
59
- ],
60
- "risks": [
61
- {
62
- "risk": "Description",
63
- "severity": "low/medium/high",
64
- "mitigation": "How to address"
65
- }
66
- ],
67
- "recommendations": ["Top 3 actionable recommendations"],
68
- "overall_health": "Score 1-10 with brief explanation"
69
- }
70
-
71
- RULES:
72
- - Base analysis on actual KB content — don't invent product details
73
- - Be realistic about market size and revenue projections
74
- - Prioritize actionable, specific recommendations over generic advice
75
- - Consider the current project phase when making recommendations
76
- - Identify concrete risks, not hypothetical concerns
77
- """
78
-
79
-
80
- def run_analysis(
81
- analysis_type: str = "full",
82
- ) -> dict:
83
- """
84
- Run strategic analysis based on the KB.
85
-
86
- Args:
87
- analysis_type: "full", "market", "revenue", or "growth"
88
-
89
- Returns:
90
- Analysis result dict.
91
- """
92
- console.print(f"[bold magenta]📊 Analyst AI[/bold magenta] — {analysis_type} analysis")
93
-
94
- # Read entire KB for context
95
- kb_content = []
96
- for cat_dir in WORKSPACE_PATH.iterdir():
97
- if not cat_dir.is_dir():
98
- continue
99
- for f in sorted(cat_dir.glob("*.md")):
100
- try:
101
- content = f.read_text(encoding="utf-8")
102
- rel = f.relative_to(WORKSPACE_PATH)
103
- kb_content.append(f"--- {rel} ---\n{content[:1000]}")
104
- except Exception:
105
- pass
106
-
107
- if not kb_content:
108
- console.print("[dim] No KB content to analyze.[/dim]")
109
- return {}
110
-
111
- # Also check for coder logs
112
- coder_root = WORKSPACE_PATH.parent / "coder"
113
- if coder_root.exists():
114
- for log_file in coder_root.rglob("log.md"):
115
- try:
116
- content = log_file.read_text(encoding="utf-8")
117
- rel = log_file.relative_to(WORKSPACE_PATH.parent)
118
- kb_content.append(f"--- {rel} ---\n{content[:500]}")
119
- except Exception:
120
- pass
121
-
122
- # Also check for test results and health reports
123
- for subdir in ["tests/results", "monitor", "reviews"]:
124
- check_dir = WORKSPACE_PATH.parent / subdir
125
- if check_dir.exists():
126
- for f in sorted(check_dir.glob("*.md"))[-3:]: # Last 3
127
- try:
128
- content = f.read_text(encoding="utf-8")
129
- rel = f.relative_to(WORKSPACE_PATH.parent)
130
- kb_content.append(f"--- {rel} ---\n{content[:500]}")
131
- except Exception:
132
- pass
133
-
134
- user_prompt = (
135
- f"## Analysis Type: {analysis_type}\n\n"
136
- f"## Knowledge Base Content\n\n"
137
- + "\n\n".join(kb_content)
138
- + "\n\nPerform the analysis. Output a JSON object."
139
- )
140
-
141
- try:
142
- raw = generate(
143
- system_prompt=SYSTEM_PROMPT,
144
- user_prompt=user_prompt,
145
- temperature=0.4,
146
- )
147
- except Exception as e:
148
- console.print(f"[red]❌ Analyst AI LLM error: {e}[/red]")
149
- return {}
150
-
151
- # Parse
152
- try:
153
- result = json.loads(raw)
154
- except json.JSONDecodeError:
155
- match = re.search(r"\{.*\}", raw, re.DOTALL)
156
- if match:
157
- try:
158
- result = json.loads(match.group())
159
- except json.JSONDecodeError:
160
- console.print("[yellow]⚠️ Analyst AI — invalid JSON[/yellow]")
161
- return {}
162
- else:
163
- return {}
164
-
165
- # Display key findings
166
- health = result.get("overall_health", "N/A")
167
- console.print(f" Project Health: {health}")
168
-
169
- recommendations = result.get("recommendations", [])
170
- if recommendations:
171
- console.print(" Top Recommendations:")
172
- for i, rec in enumerate(recommendations[:3], 1):
173
- console.print(f" {i}. {rec}")
174
-
175
- growth = result.get("growth_opportunities", [])
176
- if growth:
177
- top = sorted(growth, key=lambda x: x.get("priority", 99))[:3]
178
- console.print(f" Growth Opportunities: {len(growth)} identified")
179
-
180
- # Save report
181
- ANALYSIS_ROOT.mkdir(parents=True, exist_ok=True)
182
- ts = datetime.now().strftime("%Y%m%d_%H%M%S")
183
- report_path = ANALYSIS_ROOT / f"{analysis_type}_{ts}.md"
184
-
185
- report = (
186
- f"# {analysis_type.title()} Analysis — "
187
- f"{datetime.now().isoformat(timespec='seconds')}\n\n"
188
- )
189
-
190
- # Market
191
- market = result.get("market_analysis", {})
192
- if market:
193
- report += (
194
- f"## Market Analysis\n"
195
- f"- **Target Market**: {market.get('target_market', 'N/A')}\n"
196
- f"- **Market Size**: {market.get('market_size_estimate', 'N/A')}\n"
197
- f"- **Competitors**: {', '.join(market.get('competitors', []))}\n"
198
- f"- **Differentiators**: {', '.join(market.get('differentiators', []))}\n\n"
199
- )
200
-
201
- # Revenue
202
- revenue = result.get("revenue_strategy", {})
203
- if revenue:
204
- report += (
205
- f"## Revenue Strategy\n"
206
- f"- **Model**: {revenue.get('model', 'N/A')}\n"
207
- f"- **Timeline**: {revenue.get('revenue_timeline', 'N/A')}\n"
208
- )
209
- tiers = revenue.get("pricing_tiers", [])
210
- if tiers:
211
- report += "- **Pricing**:\n"
212
- for t in tiers:
213
- report += f" - {t.get('name', '?')}: {t.get('price', '?')} — {', '.join(t.get('features', []))}\n"
214
- report += "\n"
215
-
216
- # Growth
217
- if growth:
218
- report += "## Growth Opportunities\n"
219
- for g in growth:
220
- report += (
221
- f"- **{g.get('opportunity', '?')}** "
222
- f"(effort: {g.get('effort', '?')}, impact: {g.get('impact', '?')}, "
223
- f"priority: {g.get('priority', '?')})\n"
224
- )
225
- report += "\n"
226
-
227
- # Risks
228
- risks = result.get("risks", [])
229
- if risks:
230
- report += "## Risks\n"
231
- for r in risks:
232
- report += (
233
- f"- **{r.get('risk', '?')}** [{r.get('severity', '?')}] — "
234
- f"Mitigation: {r.get('mitigation', '?')}\n"
235
- )
236
- report += "\n"
237
-
238
- # Recommendations
239
- if recommendations:
240
- report += "## Recommendations\n"
241
- for i, rec in enumerate(recommendations, 1):
242
- report += f"{i}. {rec}\n"
243
-
244
- report_path.write_text(report, encoding="utf-8")
245
- console.print(f"[bold magenta]📊 Analyst AI[/bold magenta] — report saved")
246
-
247
- return result
1
+ """
2
+ adelie/agents/analyst_ai.py
3
+
4
+ Analyst AI — strategic analysis for market fit, revenue, and growth.
5
+
6
+ Uses LLM to analyze the entire KB and produce actionable reports:
7
+ - Market analysis & competitive landscape
8
+ - Revenue strategy & monetization optimization
9
+ - Growth opportunities & feature prioritization
10
+ - User feedback synthesis (when available)
11
+
12
+ Reports saved to .adelie/analysis/
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import re
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+
22
+ from rich.console import Console
23
+
24
+ from adelie.config import WORKSPACE_PATH
25
+ from adelie.kb import retriever
26
+ from adelie.llm_client import generate
27
+
28
+ console = Console()
29
+
30
+ ANALYSIS_ROOT = WORKSPACE_PATH.parent / "analysis"
31
+
32
+ SYSTEM_PROMPT = """You are Analyst AI — a business strategist and market analyst in an autonomous AI loop.
33
+
34
+ You receive the project's entire Knowledge Base and must produce strategic analysis.
35
+
36
+ Output a single valid JSON object:
37
+ {
38
+ "market_analysis": {
39
+ "target_market": "Description of target market and users",
40
+ "competitors": ["competitor1", "competitor2"],
41
+ "differentiators": ["unique value prop 1", "unique value prop 2"],
42
+ "market_size_estimate": "small/medium/large with reasoning"
43
+ },
44
+ "revenue_strategy": {
45
+ "model": "SaaS subscription / freemium / one-time / etc",
46
+ "pricing_tiers": [
47
+ {"name": "Free", "price": "$0", "features": ["feature1"]},
48
+ {"name": "Pro", "price": "$X/mo", "features": ["feature1", "feature2"]}
49
+ ],
50
+ "revenue_timeline": "When to expect first revenue"
51
+ },
52
+ "growth_opportunities": [
53
+ {
54
+ "opportunity": "Description",
55
+ "effort": "low/medium/high",
56
+ "impact": "low/medium/high",
57
+ "priority": 1
58
+ }
59
+ ],
60
+ "risks": [
61
+ {
62
+ "risk": "Description",
63
+ "severity": "low/medium/high",
64
+ "mitigation": "How to address"
65
+ }
66
+ ],
67
+ "recommendations": ["Top 3 actionable recommendations"],
68
+ "overall_health": "Score 1-10 with brief explanation"
69
+ }
70
+
71
+ RULES:
72
+ - Base analysis on actual KB content — don't invent product details
73
+ - Be realistic about market size and revenue projections
74
+ - Prioritize actionable, specific recommendations over generic advice
75
+ - Consider the current project phase when making recommendations
76
+ - Identify concrete risks, not hypothetical concerns
77
+ """
78
+
79
+
80
+ def run_analysis(
81
+ analysis_type: str = "full",
82
+ ) -> dict:
83
+ """
84
+ Run strategic analysis based on the KB.
85
+
86
+ Args:
87
+ analysis_type: "full", "market", "revenue", or "growth"
88
+
89
+ Returns:
90
+ Analysis result dict.
91
+ """
92
+ console.print(f"[bold magenta]📊 Analyst AI[/bold magenta] — {analysis_type} analysis")
93
+
94
+ # Read entire KB for context
95
+ kb_content = []
96
+ for cat_dir in WORKSPACE_PATH.iterdir():
97
+ if not cat_dir.is_dir():
98
+ continue
99
+ for f in sorted(cat_dir.glob("*.md")):
100
+ try:
101
+ content = f.read_text(encoding="utf-8")
102
+ rel = f.relative_to(WORKSPACE_PATH)
103
+ kb_content.append(f"--- {rel} ---\n{content[:1000]}")
104
+ except Exception:
105
+ pass
106
+
107
+ if not kb_content:
108
+ console.print("[dim] No KB content to analyze.[/dim]")
109
+ return {}
110
+
111
+ # Also check for coder logs
112
+ coder_root = WORKSPACE_PATH.parent / "coder"
113
+ if coder_root.exists():
114
+ for log_file in coder_root.rglob("log.md"):
115
+ try:
116
+ content = log_file.read_text(encoding="utf-8")
117
+ rel = log_file.relative_to(WORKSPACE_PATH.parent)
118
+ kb_content.append(f"--- {rel} ---\n{content[:500]}")
119
+ except Exception:
120
+ pass
121
+
122
+ # Also check for test results and health reports
123
+ for subdir in ["tests/results", "monitor", "reviews"]:
124
+ check_dir = WORKSPACE_PATH.parent / subdir
125
+ if check_dir.exists():
126
+ for f in sorted(check_dir.glob("*.md"))[-3:]: # Last 3
127
+ try:
128
+ content = f.read_text(encoding="utf-8")
129
+ rel = f.relative_to(WORKSPACE_PATH.parent)
130
+ kb_content.append(f"--- {rel} ---\n{content[:500]}")
131
+ except Exception:
132
+ pass
133
+
134
+ user_prompt = (
135
+ f"## Analysis Type: {analysis_type}\n\n"
136
+ f"## Knowledge Base Content\n\n"
137
+ + "\n\n".join(kb_content)
138
+ + "\n\nPerform the analysis. Output a JSON object."
139
+ )
140
+
141
+ try:
142
+ raw = generate(
143
+ system_prompt=SYSTEM_PROMPT,
144
+ user_prompt=user_prompt,
145
+ temperature=0.4,
146
+ )
147
+ except Exception as e:
148
+ console.print(f"[red]❌ Analyst AI LLM error: {e}[/red]")
149
+ return {}
150
+
151
+ # Parse
152
+ try:
153
+ result = json.loads(raw)
154
+ except json.JSONDecodeError:
155
+ match = re.search(r"\{.*\}", raw, re.DOTALL)
156
+ if match:
157
+ try:
158
+ result = json.loads(match.group())
159
+ except json.JSONDecodeError:
160
+ console.print("[yellow]⚠️ Analyst AI — invalid JSON[/yellow]")
161
+ return {}
162
+ else:
163
+ return {}
164
+
165
+ # Display key findings
166
+ health = result.get("overall_health", "N/A")
167
+ console.print(f" Project Health: {health}")
168
+
169
+ recommendations = result.get("recommendations", [])
170
+ if recommendations:
171
+ console.print(" Top Recommendations:")
172
+ for i, rec in enumerate(recommendations[:3], 1):
173
+ console.print(f" {i}. {rec}")
174
+
175
+ growth = result.get("growth_opportunities", [])
176
+ if growth:
177
+ top = sorted(growth, key=lambda x: x.get("priority", 99))[:3]
178
+ console.print(f" Growth Opportunities: {len(growth)} identified")
179
+
180
+ # Save report
181
+ ANALYSIS_ROOT.mkdir(parents=True, exist_ok=True)
182
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
183
+ report_path = ANALYSIS_ROOT / f"{analysis_type}_{ts}.md"
184
+
185
+ report = (
186
+ f"# {analysis_type.title()} Analysis — "
187
+ f"{datetime.now().isoformat(timespec='seconds')}\n\n"
188
+ )
189
+
190
+ # Market
191
+ market = result.get("market_analysis", {})
192
+ if market:
193
+ report += (
194
+ f"## Market Analysis\n"
195
+ f"- **Target Market**: {market.get('target_market', 'N/A')}\n"
196
+ f"- **Market Size**: {market.get('market_size_estimate', 'N/A')}\n"
197
+ f"- **Competitors**: {', '.join(market.get('competitors', []))}\n"
198
+ f"- **Differentiators**: {', '.join(market.get('differentiators', []))}\n\n"
199
+ )
200
+
201
+ # Revenue
202
+ revenue = result.get("revenue_strategy", {})
203
+ if revenue:
204
+ report += (
205
+ f"## Revenue Strategy\n"
206
+ f"- **Model**: {revenue.get('model', 'N/A')}\n"
207
+ f"- **Timeline**: {revenue.get('revenue_timeline', 'N/A')}\n"
208
+ )
209
+ tiers = revenue.get("pricing_tiers", [])
210
+ if tiers:
211
+ report += "- **Pricing**:\n"
212
+ for t in tiers:
213
+ report += f" - {t.get('name', '?')}: {t.get('price', '?')} — {', '.join(t.get('features', []))}\n"
214
+ report += "\n"
215
+
216
+ # Growth
217
+ if growth:
218
+ report += "## Growth Opportunities\n"
219
+ for g in growth:
220
+ report += (
221
+ f"- **{g.get('opportunity', '?')}** "
222
+ f"(effort: {g.get('effort', '?')}, impact: {g.get('impact', '?')}, "
223
+ f"priority: {g.get('priority', '?')})\n"
224
+ )
225
+ report += "\n"
226
+
227
+ # Risks
228
+ risks = result.get("risks", [])
229
+ if risks:
230
+ report += "## Risks\n"
231
+ for r in risks:
232
+ report += (
233
+ f"- **{r.get('risk', '?')}** [{r.get('severity', '?')}] — "
234
+ f"Mitigation: {r.get('mitigation', '?')}\n"
235
+ )
236
+ report += "\n"
237
+
238
+ # Recommendations
239
+ if recommendations:
240
+ report += "## Recommendations\n"
241
+ for i, rec in enumerate(recommendations, 1):
242
+ report += f"{i}. {rec}\n"
243
+
244
+ report_path.write_text(report, encoding="utf-8")
245
+ console.print(f"[bold magenta]📊 Analyst AI[/bold magenta] — report saved")
246
+
247
+ return result