devrel-origin 0.2.14__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 (98) hide show
  1. devrel_origin/__init__.py +15 -0
  2. devrel_origin/cli/__init__.py +92 -0
  3. devrel_origin/cli/_common.py +243 -0
  4. devrel_origin/cli/analytics.py +28 -0
  5. devrel_origin/cli/argus.py +497 -0
  6. devrel_origin/cli/auth.py +227 -0
  7. devrel_origin/cli/config.py +108 -0
  8. devrel_origin/cli/content.py +259 -0
  9. devrel_origin/cli/cost.py +108 -0
  10. devrel_origin/cli/cro.py +298 -0
  11. devrel_origin/cli/deliverables.py +65 -0
  12. devrel_origin/cli/docs.py +91 -0
  13. devrel_origin/cli/doctor.py +178 -0
  14. devrel_origin/cli/experiment.py +29 -0
  15. devrel_origin/cli/growth.py +97 -0
  16. devrel_origin/cli/init.py +472 -0
  17. devrel_origin/cli/intel.py +27 -0
  18. devrel_origin/cli/kb.py +96 -0
  19. devrel_origin/cli/listen.py +31 -0
  20. devrel_origin/cli/marketing.py +66 -0
  21. devrel_origin/cli/migrate.py +45 -0
  22. devrel_origin/cli/run.py +46 -0
  23. devrel_origin/cli/sales.py +57 -0
  24. devrel_origin/cli/schedule.py +62 -0
  25. devrel_origin/cli/synthesize.py +28 -0
  26. devrel_origin/cli/triage.py +29 -0
  27. devrel_origin/cli/video.py +35 -0
  28. devrel_origin/core/__init__.py +58 -0
  29. devrel_origin/core/agent_config.py +75 -0
  30. devrel_origin/core/argus.py +964 -0
  31. devrel_origin/core/atlas.py +1450 -0
  32. devrel_origin/core/base.py +372 -0
  33. devrel_origin/core/cyra.py +563 -0
  34. devrel_origin/core/dex.py +708 -0
  35. devrel_origin/core/echo.py +614 -0
  36. devrel_origin/core/growth/__init__.py +27 -0
  37. devrel_origin/core/growth/recommendations.py +219 -0
  38. devrel_origin/core/growth/target_kinds.py +51 -0
  39. devrel_origin/core/iris.py +513 -0
  40. devrel_origin/core/kai.py +1367 -0
  41. devrel_origin/core/llm.py +542 -0
  42. devrel_origin/core/llm_backends.py +274 -0
  43. devrel_origin/core/mox.py +514 -0
  44. devrel_origin/core/nova.py +349 -0
  45. devrel_origin/core/pax.py +1205 -0
  46. devrel_origin/core/rex.py +532 -0
  47. devrel_origin/core/sage.py +486 -0
  48. devrel_origin/core/sentinel.py +385 -0
  49. devrel_origin/core/types.py +98 -0
  50. devrel_origin/core/video/__init__.py +22 -0
  51. devrel_origin/core/video/assembler.py +131 -0
  52. devrel_origin/core/video/browser_recorder.py +118 -0
  53. devrel_origin/core/video/desktop_recorder.py +254 -0
  54. devrel_origin/core/video/overlay_renderer.py +143 -0
  55. devrel_origin/core/video/script_parser.py +147 -0
  56. devrel_origin/core/video/tts_engine.py +82 -0
  57. devrel_origin/core/vox.py +268 -0
  58. devrel_origin/core/watchdog.py +321 -0
  59. devrel_origin/project/__init__.py +1 -0
  60. devrel_origin/project/config.py +75 -0
  61. devrel_origin/project/cost_sink.py +61 -0
  62. devrel_origin/project/init.py +104 -0
  63. devrel_origin/project/paths.py +75 -0
  64. devrel_origin/project/state.py +241 -0
  65. devrel_origin/project/templates/__init__.py +4 -0
  66. devrel_origin/project/templates/config.toml +24 -0
  67. devrel_origin/project/templates/devrel.gitignore +10 -0
  68. devrel_origin/project/templates/slop-blocklist.md +45 -0
  69. devrel_origin/project/templates/style.md +24 -0
  70. devrel_origin/project/templates/voice.md +29 -0
  71. devrel_origin/quality/__init__.py +66 -0
  72. devrel_origin/quality/editorial.py +357 -0
  73. devrel_origin/quality/persona.py +84 -0
  74. devrel_origin/quality/readability.py +148 -0
  75. devrel_origin/quality/slop.py +167 -0
  76. devrel_origin/quality/style.py +110 -0
  77. devrel_origin/quality/voice.py +15 -0
  78. devrel_origin/tools/__init__.py +9 -0
  79. devrel_origin/tools/analytics.py +304 -0
  80. devrel_origin/tools/api_client.py +393 -0
  81. devrel_origin/tools/apollo_client.py +305 -0
  82. devrel_origin/tools/code_validator.py +428 -0
  83. devrel_origin/tools/github_tools.py +297 -0
  84. devrel_origin/tools/instantly_client.py +412 -0
  85. devrel_origin/tools/kb_harvester.py +340 -0
  86. devrel_origin/tools/mcp_server.py +578 -0
  87. devrel_origin/tools/notifications.py +245 -0
  88. devrel_origin/tools/run_report.py +193 -0
  89. devrel_origin/tools/scheduler.py +231 -0
  90. devrel_origin/tools/search_tools.py +321 -0
  91. devrel_origin/tools/self_improve.py +168 -0
  92. devrel_origin/tools/sheets.py +236 -0
  93. devrel_origin-0.2.14.dist-info/METADATA +354 -0
  94. devrel_origin-0.2.14.dist-info/RECORD +98 -0
  95. devrel_origin-0.2.14.dist-info/WHEEL +5 -0
  96. devrel_origin-0.2.14.dist-info/entry_points.txt +2 -0
  97. devrel_origin-0.2.14.dist-info/licenses/LICENSE +21 -0
  98. devrel_origin-0.2.14.dist-info/top_level.txt +1 -0
@@ -0,0 +1,31 @@
1
+ """`devrel listen` — social-media listening via Echo."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from devrel_origin.cli._common import build_atlas_or_exit, find_paths_or_exit, render_result
11
+
12
+ console = Console()
13
+
14
+
15
+ def listen_command(
16
+ platforms: str = typer.Option(
17
+ "reddit,hn,twitter",
18
+ "--platforms",
19
+ help="Comma-separated platforms to scan.",
20
+ ),
21
+ json_output: bool = typer.Option(False, "--json"),
22
+ ) -> None:
23
+ """Scan social media for product mentions and sentiment."""
24
+ paths = find_paths_or_exit(console)
25
+ atlas = build_atlas_or_exit(paths, console)
26
+
27
+ async def _do() -> None:
28
+ result = await atlas.run_single_task("echo", f"Scan {platforms} for product mentions")
29
+ render_result(result, console, json_output=json_output)
30
+
31
+ asyncio.run(_do())
@@ -0,0 +1,66 @@
1
+ """`devrel marketing {blog, landing, social, campaign}` — Mox-powered surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from devrel_origin.cli._common import build_atlas_or_exit, find_paths_or_exit, render_result
11
+
12
+ console = Console()
13
+
14
+ marketing_app = typer.Typer(
15
+ name="marketing",
16
+ help="Marketing campaigns: blog posts, landing pages, social, full campaigns.",
17
+ no_args_is_help=True,
18
+ add_completion=False,
19
+ )
20
+
21
+
22
+ def _run(task: str, json_output: bool) -> None:
23
+ paths = find_paths_or_exit(console)
24
+ atlas = build_atlas_or_exit(paths, console)
25
+
26
+ async def _do() -> None:
27
+ result = await atlas.run_single_task("mox", task)
28
+ render_result(result, console, json_output=json_output)
29
+
30
+ asyncio.run(_do())
31
+
32
+
33
+ @marketing_app.command("blog")
34
+ def blog(
35
+ topic: str = typer.Argument(..., help="Blog topic."),
36
+ json_output: bool = typer.Option(False, "--json"),
37
+ ) -> None:
38
+ """Write a blog post on a topic."""
39
+ _run(f"Write blog post: {topic}", json_output)
40
+
41
+
42
+ @marketing_app.command("landing")
43
+ def landing(
44
+ topic: str = typer.Argument(..., help="Landing page topic."),
45
+ json_output: bool = typer.Option(False, "--json"),
46
+ ) -> None:
47
+ """Write landing page copy."""
48
+ _run(f"Write landing page copy: {topic}", json_output)
49
+
50
+
51
+ @marketing_app.command("social")
52
+ def social(
53
+ topic: str = typer.Argument(..., help="Social topic."),
54
+ json_output: bool = typer.Option(False, "--json"),
55
+ ) -> None:
56
+ """Write a social media batch."""
57
+ _run(f"Write social batch: {topic}", json_output)
58
+
59
+
60
+ @marketing_app.command("campaign")
61
+ def campaign(
62
+ brief: str = typer.Argument(..., help="Campaign brief."),
63
+ json_output: bool = typer.Option(False, "--json"),
64
+ ) -> None:
65
+ """Build a full marketing campaign."""
66
+ _run(f"Build campaign: {brief}", json_output)
@@ -0,0 +1,45 @@
1
+ """`devrel migrate` - upgrade the project state.db schema in place.
2
+
3
+ State DBs created by older devrel-origin releases (e.g. 0.2.4 / schema v4)
4
+ must be upgraded to the current SCHEMA_VERSION before agents can write to
5
+ the new tables. `init_db()` is idempotent and runs the v5 migration on any
6
+ existing DB; this verb just exposes it as a discoverable CLI command so
7
+ users don't have to import internals.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import typer
13
+ from rich.console import Console
14
+
15
+ from devrel_origin.cli._common import find_paths_or_exit
16
+ from devrel_origin.project.state import SCHEMA_VERSION, get_schema_version, init_db
17
+
18
+ console = Console()
19
+
20
+
21
+ def migrate_command() -> None:
22
+ """Upgrade the project state.db schema to the current version."""
23
+ paths = find_paths_or_exit(console)
24
+ db = paths.state_db
25
+
26
+ before = get_schema_version(db) if db.is_file() else None
27
+ if before == SCHEMA_VERSION:
28
+ console.print(
29
+ f"[green]✓[/green] state.db already at schema v{SCHEMA_VERSION}; nothing to migrate."
30
+ )
31
+ return
32
+
33
+ init_db(db)
34
+ after = get_schema_version(db)
35
+
36
+ if after != SCHEMA_VERSION:
37
+ console.print(
38
+ f"[red]✗[/red] migration ran but schema version is v{after}, expected v{SCHEMA_VERSION}."
39
+ )
40
+ raise typer.Exit(code=1)
41
+
42
+ if before is None:
43
+ console.print(f"[green]✓[/green] state.db created at schema v{after}.")
44
+ else:
45
+ console.print(f"[green]✓[/green] state.db migrated v{before} -> v{after}.")
@@ -0,0 +1,46 @@
1
+ """`devrel run` — full weekly pipeline, health check, or single agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from devrel_origin.cli._common import build_atlas_or_exit, find_paths_or_exit
11
+
12
+ console = Console()
13
+
14
+
15
+ def run_command(
16
+ health: bool = typer.Option(False, "--health", help="Only run the Watchdog health check."),
17
+ agent: str = typer.Option("", "--agent", help="Run a single agent by name (e.g., 'kai')."),
18
+ task: str = typer.Option("", "--task", help="Task description for --agent."),
19
+ ) -> None:
20
+ """Run the full weekly pipeline (default), or a subset via flags."""
21
+ paths = find_paths_or_exit(console)
22
+ atlas = build_atlas_or_exit(paths, console)
23
+
24
+ async def _do() -> None:
25
+ if health:
26
+ result = await atlas.run_single_task("watchdog", "Check system health")
27
+ console.print(
28
+ f"[green]✓[/green] watchdog: {str(result.output)[:300]}"
29
+ if result.success
30
+ else f"[red]✗[/red] {result.error}"
31
+ )
32
+ return
33
+ if agent:
34
+ t = task or f"Run {agent} with default settings"
35
+ result = await atlas.run_single_task(agent, t)
36
+ if result.success:
37
+ console.print(f"[green]✓[/green] {agent}: {str(result.output)[:300]}")
38
+ else:
39
+ console.print(f"[red]✗[/red] {agent} failed: {result.error}")
40
+ raise typer.Exit(code=1)
41
+ return
42
+ # Full weekly pipeline.
43
+ ctx = await atlas.run_weekly_cycle()
44
+ console.print(f"[bold green]Weekly cycle complete.[/bold green] week_of={ctx.week_of}")
45
+
46
+ asyncio.run(_do())
@@ -0,0 +1,57 @@
1
+ """`devrel sales {outreach, battlecard, sequence}` — Pax-powered sales surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from devrel_origin.cli._common import build_atlas_or_exit, find_paths_or_exit, render_result
11
+
12
+ console = Console()
13
+
14
+ sales_app = typer.Typer(
15
+ name="sales",
16
+ help="Sales enablement: outreach, battle cards, nurture sequences.",
17
+ no_args_is_help=True,
18
+ add_completion=False,
19
+ )
20
+
21
+
22
+ def _run(task: str, json_output: bool) -> None:
23
+ paths = find_paths_or_exit(console)
24
+ atlas = build_atlas_or_exit(paths, console)
25
+
26
+ async def _do() -> None:
27
+ result = await atlas.run_single_task("pax", task)
28
+ render_result(result, console, json_output=json_output)
29
+
30
+ asyncio.run(_do())
31
+
32
+
33
+ @sales_app.command("outreach")
34
+ def outreach(
35
+ company: str = typer.Argument(..., help="Target company."),
36
+ json_output: bool = typer.Option(False, "--json"),
37
+ ) -> None:
38
+ """Draft a cold outreach email for a target company."""
39
+ _run(f"Draft outreach email for {company}", json_output)
40
+
41
+
42
+ @sales_app.command("battlecard")
43
+ def battlecard(
44
+ competitor: str = typer.Argument(..., help="Competitor."),
45
+ json_output: bool = typer.Option(False, "--json"),
46
+ ) -> None:
47
+ """Build a sales battle card against a competitor."""
48
+ _run(f"Build battle card vs. {competitor}", json_output)
49
+
50
+
51
+ @sales_app.command("sequence")
52
+ def sequence(
53
+ campaign: str = typer.Argument(..., help="Campaign description."),
54
+ json_output: bool = typer.Option(False, "--json"),
55
+ ) -> None:
56
+ """Design a multi-touch nurture sequence."""
57
+ _run(f"Design nurture sequence: {campaign}", json_output)
@@ -0,0 +1,62 @@
1
+ """`devrel schedule {install, list, remove}` — wraps tools.scheduler.Scheduler.
2
+
3
+ The Scheduler ctor accepts `project_dir` (string path); we pass the
4
+ project root resolved from `.devrel/`. The CLI is a thin wrapper around
5
+ `install_cron`, `remove_cron`, and `list_entries`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import typer
11
+ from rich.console import Console
12
+
13
+ from devrel_origin.cli._common import find_paths_or_exit
14
+ from devrel_origin.tools.scheduler import Scheduler
15
+
16
+ console = Console()
17
+
18
+ schedule_app = typer.Typer(
19
+ name="schedule",
20
+ help="Manage cron-based agent scheduling.",
21
+ no_args_is_help=True,
22
+ add_completion=False,
23
+ )
24
+
25
+
26
+ def _build_scheduler() -> Scheduler:
27
+ paths = find_paths_or_exit(console)
28
+ return Scheduler(project_dir=str(paths.root))
29
+
30
+
31
+ @schedule_app.command("install")
32
+ def install() -> None:
33
+ """Install agent schedule into the user crontab."""
34
+ sched = _build_scheduler()
35
+ lines = sched.install_cron()
36
+ console.print(f"[green]✓[/green] Installed {len(lines)} cron entry(ies)")
37
+ for line in lines:
38
+ console.print(f" [dim]{line}[/dim]")
39
+
40
+
41
+ @schedule_app.command("list")
42
+ def list_entries() -> None:
43
+ """List the configured schedule entries."""
44
+ sched = _build_scheduler()
45
+ entries = sched.list_entries()
46
+ if not entries:
47
+ console.print("[yellow]No schedule entries configured.[/yellow]")
48
+ return
49
+ for e in entries:
50
+ flag = "[green]on[/green]" if e.get("enabled") else "[dim]off[/dim]"
51
+ console.print(
52
+ f" {flag} [bold]{e.get('name', '?')}[/bold] "
53
+ f"[dim]{e.get('cron', '?')}[/dim] {e.get('description', '')}"
54
+ )
55
+
56
+
57
+ @schedule_app.command("remove")
58
+ def remove() -> None:
59
+ """Remove all devrel-origin entries from the user crontab."""
60
+ sched = _build_scheduler()
61
+ sched.remove_cron()
62
+ console.print("[green]✓[/green] Removed devrel-origin cron entries")
@@ -0,0 +1,28 @@
1
+ """`devrel synthesize` — theme extraction via Iris."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from devrel_origin.cli._common import build_atlas_or_exit, find_paths_or_exit, render_result
11
+
12
+ console = Console()
13
+
14
+
15
+ def synthesize_command(
16
+ json_output: bool = typer.Option(False, "--json"),
17
+ ) -> None:
18
+ """Extract themes from latest social + triage signals via Iris."""
19
+ paths = find_paths_or_exit(console)
20
+ atlas = build_atlas_or_exit(paths, console)
21
+
22
+ async def _do() -> None:
23
+ result = await atlas.run_single_task(
24
+ "iris", "Extract themes from latest social + triage signals"
25
+ )
26
+ render_result(result, console, json_output=json_output)
27
+
28
+ asyncio.run(_do())
@@ -0,0 +1,29 @@
1
+ """`devrel triage` — GitHub issue triage via Sage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from devrel_origin.cli._common import build_atlas_or_exit, find_paths_or_exit, render_result
11
+
12
+ console = Console()
13
+
14
+
15
+ def triage_command(
16
+ days: int = typer.Option(7, "--days", help="Look back this many days."),
17
+ json_output: bool = typer.Option(False, "--json"),
18
+ ) -> None:
19
+ """Triage GitHub issues from the last N days."""
20
+ paths = find_paths_or_exit(console)
21
+ atlas = build_atlas_or_exit(paths, console)
22
+
23
+ async def _do() -> None:
24
+ result = await atlas.run_single_task(
25
+ "sage", f"Triage GitHub issues from the last {days} days"
26
+ )
27
+ render_result(result, console, json_output=json_output)
28
+
29
+ asyncio.run(_do())
@@ -0,0 +1,35 @@
1
+ """`devrel video record` — screen-recorded tutorials via Vox."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from devrel_origin.cli._common import build_atlas_or_exit, find_paths_or_exit, render_result
11
+
12
+ console = Console()
13
+
14
+ video_app = typer.Typer(
15
+ name="video",
16
+ help="Video tutorial production.",
17
+ no_args_is_help=True,
18
+ add_completion=False,
19
+ )
20
+
21
+
22
+ @video_app.command("record")
23
+ def record(
24
+ script: str = typer.Argument(..., help="Path to script markdown OR raw task description."),
25
+ json_output: bool = typer.Option(False, "--json"),
26
+ ) -> None:
27
+ """Record a screen-recorded video tutorial via Vox."""
28
+ paths = find_paths_or_exit(console)
29
+ atlas = build_atlas_or_exit(paths, console)
30
+
31
+ async def _do() -> None:
32
+ result = await atlas.run_single_task("vox", f"Record video tutorial: {script}")
33
+ render_result(result, console, json_output=json_output)
34
+
35
+ asyncio.run(_do())
@@ -0,0 +1,58 @@
1
+ """
2
+ DevTools Advocate Agent System
3
+
4
+ A multi-agent system for autonomous developer advocacy,
5
+ built on Claude Agent SDK and Model Context Protocol (MCP).
6
+ """
7
+
8
+ from devrel_origin.core.argus import (
9
+ Argus,
10
+ PerformanceMetric,
11
+ PerformanceReport,
12
+ Recommendation,
13
+ )
14
+ from devrel_origin.core.atlas import Atlas
15
+ from devrel_origin.core.cyra import (
16
+ CroReport,
17
+ Cyra,
18
+ DropOff,
19
+ FunnelStep,
20
+ Hypothesis,
21
+ )
22
+ from devrel_origin.core.dex import Dex
23
+ from devrel_origin.core.echo import Echo
24
+ from devrel_origin.core.iris import Iris
25
+ from devrel_origin.core.kai import Kai
26
+ from devrel_origin.core.mox import Mox
27
+ from devrel_origin.core.nova import Nova
28
+ from devrel_origin.core.pax import Pax
29
+ from devrel_origin.core.rex import Rex
30
+ from devrel_origin.core.sage import Sage
31
+ from devrel_origin.core.sentinel import Sentinel
32
+ from devrel_origin.core.vox import Vox
33
+ from devrel_origin.core.watchdog import Watchdog
34
+
35
+ __all__ = [
36
+ "Argus",
37
+ "Atlas",
38
+ "Cyra",
39
+ "Dex",
40
+ "Echo",
41
+ "Kai",
42
+ "Mox",
43
+ "Sage",
44
+ "Iris",
45
+ "Nova",
46
+ "Pax",
47
+ "Rex",
48
+ "Sentinel",
49
+ "Vox",
50
+ "Watchdog",
51
+ "CroReport",
52
+ "DropOff",
53
+ "FunnelStep",
54
+ "Hypothesis",
55
+ "PerformanceMetric",
56
+ "PerformanceReport",
57
+ "Recommendation",
58
+ ]
@@ -0,0 +1,75 @@
1
+ """Agent configuration loader from YAML."""
2
+
3
+ import logging
4
+ import os
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import yaml
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ DEFAULT_WORKFLOW_ORDER = ["sage", "iris", "nova", "kai"]
14
+ DEFAULT_AGENT_CONFIG: dict[str, Any] = {
15
+ "temperature": 0.7,
16
+ "model": "claude-sonnet-4-5-20250929",
17
+ "max_tokens": 4096,
18
+ }
19
+ DEFAULT_RETRY = {
20
+ "max_retries": 3,
21
+ "initial_delay_seconds": 5,
22
+ "backoff_multiplier": 2.0,
23
+ "max_delay_seconds": 60,
24
+ }
25
+
26
+
27
+ @dataclass
28
+ class AgentConfig:
29
+ """Parsed agent configuration."""
30
+
31
+ product_name: str = os.getenv("PRODUCT_NAME", "OpenClaw")
32
+ product_url: str = os.getenv("PRODUCT_URL", "https://openclaw.ai")
33
+ budget_limit_usd: float = 10.0 # Weekly budget; 0 = unlimited
34
+ agents: dict[str, Any] = field(default_factory=dict)
35
+ workflow_order: list[str] = field(default_factory=lambda: list(DEFAULT_WORKFLOW_ORDER))
36
+ retry_settings: dict[str, Any] = field(default_factory=lambda: dict(DEFAULT_RETRY))
37
+ api_clients: dict[str, Any] = field(default_factory=dict)
38
+ logging_config: dict[str, Any] = field(default_factory=dict)
39
+ analytics_in_run: bool = True
40
+ cro_in_run: bool = False # Stage 5c: Cyra CRO audit (opt-in; Vega+Selene added in Waves 2/3)
41
+ agent_timeouts: dict[str, float] = field(
42
+ default_factory=dict
43
+ ) # per-agent timeout override (seconds)
44
+
45
+ def get_agent_config(self, agent_name: str) -> dict[str, Any]:
46
+ """Get config for a specific agent, merging with defaults."""
47
+ defaults = dict(DEFAULT_AGENT_CONFIG)
48
+ overrides = self.agents.get(agent_name, {})
49
+ return {**defaults, **overrides}
50
+
51
+
52
+ def load_config(path: Path) -> AgentConfig:
53
+ """Load config from YAML file, falling back to defaults."""
54
+ if not path.exists():
55
+ logger.warning(f"Config not found at {path}, using defaults")
56
+ return AgentConfig()
57
+
58
+ with open(path) as f:
59
+ raw = yaml.safe_load(f) or {}
60
+
61
+ return AgentConfig(
62
+ product_name=raw.get("product_name", os.getenv("PRODUCT_NAME", "OpenClaw")),
63
+ product_url=raw.get("product_url", os.getenv("PRODUCT_URL", "https://openclaw.ai")),
64
+ budget_limit_usd=raw.get("budget_limit_usd", 10.0),
65
+ agents=raw.get("agents", {}),
66
+ workflow_order=raw.get("orchestration", {}).get("workflow_order", DEFAULT_WORKFLOW_ORDER),
67
+ retry_settings={**DEFAULT_RETRY, **raw.get("retry_settings", {})},
68
+ api_clients=raw.get("api_clients", {}),
69
+ logging_config=raw.get("logging", {}),
70
+ analytics_in_run=raw.get("orchestration", {}).get("analytics_in_run", True),
71
+ cro_in_run=raw.get("orchestration", {}).get("cro_in_run", False),
72
+ agent_timeouts={
73
+ k: float(v) for k, v in raw.get("orchestration", {}).get("agent_timeouts", {}).items()
74
+ },
75
+ )