devrel-origin 0.2.14__tar.gz → 0.2.16__tar.gz
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.
- {devrel_origin-0.2.14/src/devrel_origin.egg-info → devrel_origin-0.2.16}/PKG-INFO +1 -1
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/pyproject.toml +1 -1
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/doctor.py +21 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/argus.py +45 -12
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/atlas.py +60 -14
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/vox.py +52 -2
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/project/state.py +110 -1
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/quality/editorial.py +8 -9
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/analytics.py +20 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/api_client.py +85 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/run_report.py +4 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16/src/devrel_origin.egg-info}/PKG-INFO +1 -1
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_argus.py +27 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_atlas.py +15 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_integration.py +14 -1
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_vox.py +17 -1
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/LICENSE +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/README.md +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/setup.cfg +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/__init__.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/__init__.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/_common.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/analytics.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/argus.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/auth.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/config.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/content.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/cost.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/cro.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/deliverables.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/docs.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/experiment.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/growth.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/init.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/intel.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/kb.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/listen.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/marketing.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/migrate.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/run.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/sales.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/schedule.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/synthesize.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/triage.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/cli/video.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/__init__.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/agent_config.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/base.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/cyra.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/dex.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/echo.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/growth/__init__.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/growth/recommendations.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/growth/target_kinds.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/iris.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/kai.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/llm.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/llm_backends.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/mox.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/nova.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/pax.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/rex.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/sage.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/sentinel.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/types.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/video/__init__.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/video/assembler.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/video/browser_recorder.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/video/desktop_recorder.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/video/overlay_renderer.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/video/script_parser.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/video/tts_engine.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/core/watchdog.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/project/__init__.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/project/config.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/project/cost_sink.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/project/init.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/project/paths.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/project/templates/__init__.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/project/templates/config.toml +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/project/templates/devrel.gitignore +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/project/templates/slop-blocklist.md +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/project/templates/style.md +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/project/templates/voice.md +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/quality/__init__.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/quality/persona.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/quality/readability.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/quality/slop.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/quality/style.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/quality/voice.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/__init__.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/apollo_client.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/code_validator.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/github_tools.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/instantly_client.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/kb_harvester.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/mcp_server.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/notifications.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/scheduler.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/search_tools.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/self_improve.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin/tools/sheets.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin.egg-info/SOURCES.txt +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin.egg-info/dependency_links.txt +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin.egg-info/entry_points.txt +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin.egg-info/requires.txt +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/src/devrel_origin.egg-info/top_level.txt +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_agent_edge_cases.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_analytics_collectors.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_api_client.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_apollo_client.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_atlas_replies.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_base_agent.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_code_validator.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_config.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_cyra.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_dex.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_echo.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_github_tools.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_instantly_client.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_iris.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_kai.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_llm.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_llm_backends.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_llm_cost_sink.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_llm_cost_tracking.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_mcp_server.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_mox.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_mox_instantly.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_nova.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_pax.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_pax_apollo.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_pax_instantly.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_rex.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_rex_apollo.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_sage.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_search_tools.py +0 -0
- {devrel_origin-0.2.14 → devrel_origin-0.2.16}/tests/test_sentinel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devrel-origin
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.16
|
|
4
4
|
Summary: A 15-agent CLI that runs DevRel, sales, and marketing on your repo. BYO Anthropic or OpenRouter key.
|
|
5
5
|
Author-email: Daria Dovzhikova <dovzhikova@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devrel-origin"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.16"
|
|
8
8
|
description = "A 15-agent CLI that runs DevRel, sales, and marketing on your repo. BYO Anthropic or OpenRouter key."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -4,8 +4,10 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import shutil
|
|
7
8
|
import sys
|
|
8
9
|
from dataclasses import asdict, dataclass
|
|
10
|
+
from importlib.util import find_spec
|
|
9
11
|
|
|
10
12
|
import typer
|
|
11
13
|
from rich.console import Console
|
|
@@ -118,6 +120,25 @@ def _run_checks(paths: ProjectPaths) -> list[CheckResult]:
|
|
|
118
120
|
else:
|
|
119
121
|
results.append(CheckResult("kb_files", "warn", "kb/ missing"))
|
|
120
122
|
|
|
123
|
+
# Optional video toolchain for Vox. Missing pieces should never block the
|
|
124
|
+
# core content pipeline, but doctor should tell users the exact render fix.
|
|
125
|
+
if shutil.which("ffmpeg"):
|
|
126
|
+
results.append(CheckResult("video_ffmpeg", "pass", "installed"))
|
|
127
|
+
else:
|
|
128
|
+
results.append(
|
|
129
|
+
CheckResult("video_ffmpeg", "warn", "not installed; run `brew install ffmpeg`")
|
|
130
|
+
)
|
|
131
|
+
if find_spec("playwright") is not None:
|
|
132
|
+
results.append(CheckResult("video_playwright", "pass", "installed"))
|
|
133
|
+
else:
|
|
134
|
+
results.append(
|
|
135
|
+
CheckResult(
|
|
136
|
+
"video_playwright",
|
|
137
|
+
"warn",
|
|
138
|
+
"not installed; run `pip install 'devrel-origin\\[video]' && python -m playwright install chromium`",
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
121
142
|
return results
|
|
122
143
|
|
|
123
144
|
|
|
@@ -76,6 +76,10 @@ class Recommendation:
|
|
|
76
76
|
source_ids: list[str] = field(default_factory=list)
|
|
77
77
|
first_seen_period: str | None = None # set by _persist_sync; ISO timestamp
|
|
78
78
|
|
|
79
|
+
def __post_init__(self) -> None:
|
|
80
|
+
self.evidence = _coerce_str_list(self.evidence)
|
|
81
|
+
self.source_ids = _coerce_str_list(self.source_ids)
|
|
82
|
+
|
|
79
83
|
|
|
80
84
|
@dataclass
|
|
81
85
|
class PerformanceReport:
|
|
@@ -114,15 +118,40 @@ def _metric_to_jsonable(m: PerformanceMetric) -> dict:
|
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
|
|
121
|
+
def _coerce_str_list(value: Any) -> list[str]:
|
|
122
|
+
if value is None:
|
|
123
|
+
return []
|
|
124
|
+
if isinstance(value, str):
|
|
125
|
+
return [value]
|
|
126
|
+
if isinstance(value, (list, tuple, set)):
|
|
127
|
+
items = value
|
|
128
|
+
else:
|
|
129
|
+
items = [value]
|
|
130
|
+
|
|
131
|
+
out: list[str] = []
|
|
132
|
+
for item in items:
|
|
133
|
+
if item is None:
|
|
134
|
+
continue
|
|
135
|
+
if isinstance(item, str):
|
|
136
|
+
text = item
|
|
137
|
+
elif isinstance(item, (dict, list, tuple, set)):
|
|
138
|
+
text = json.dumps(item, sort_keys=True)
|
|
139
|
+
else:
|
|
140
|
+
text = str(item)
|
|
141
|
+
if text:
|
|
142
|
+
out.append(text)
|
|
143
|
+
return out
|
|
144
|
+
|
|
145
|
+
|
|
117
146
|
def _rec_to_jsonable(r: Recommendation) -> dict:
|
|
118
147
|
return {
|
|
119
148
|
"action": r.action,
|
|
120
149
|
"target": r.target,
|
|
121
150
|
"target_type": r.target_type,
|
|
122
151
|
"rationale": r.rationale,
|
|
123
|
-
"evidence":
|
|
152
|
+
"evidence": _coerce_str_list(r.evidence),
|
|
124
153
|
"confidence": r.confidence,
|
|
125
|
-
"source_ids":
|
|
154
|
+
"source_ids": _coerce_str_list(r.source_ids),
|
|
126
155
|
"first_seen_period": r.first_seen_period,
|
|
127
156
|
}
|
|
128
157
|
|
|
@@ -133,7 +162,7 @@ def _report_to_jsonable(r: PerformanceReport) -> dict:
|
|
|
133
162
|
"period_end": r.period_end.isoformat(),
|
|
134
163
|
"top_performers": [_metric_to_jsonable(m) for m in r.top_performers],
|
|
135
164
|
"bottom_performers": [_metric_to_jsonable(m) for m in r.bottom_performers],
|
|
136
|
-
"trend_signals":
|
|
165
|
+
"trend_signals": _coerce_str_list(r.trend_signals),
|
|
137
166
|
"recommendations": [_rec_to_jsonable(rec) for rec in r.recommendations],
|
|
138
167
|
"sources_ok": dict(r.sources_ok),
|
|
139
168
|
"insufficient_data": r.insufficient_data,
|
|
@@ -374,14 +403,16 @@ def _render_brief(rec: Recommendation, period: str) -> str:
|
|
|
374
403
|
lines.append("## Why")
|
|
375
404
|
lines.append(rec.rationale)
|
|
376
405
|
lines.append("")
|
|
377
|
-
|
|
406
|
+
evidence = _coerce_str_list(rec.evidence)
|
|
407
|
+
if evidence:
|
|
378
408
|
lines.append("## Evidence")
|
|
379
|
-
for ev in
|
|
409
|
+
for ev in evidence:
|
|
380
410
|
lines.append(f"- {ev}")
|
|
381
411
|
lines.append("")
|
|
382
|
-
|
|
412
|
+
source_ids = _coerce_str_list(rec.source_ids)
|
|
413
|
+
if source_ids:
|
|
383
414
|
lines.append("## Source content")
|
|
384
|
-
for sid in
|
|
415
|
+
for sid in source_ids:
|
|
385
416
|
lines.append(f"- `{sid}`")
|
|
386
417
|
lines.append("")
|
|
387
418
|
lines.append("## Next step")
|
|
@@ -441,9 +472,10 @@ def _render_markdown(report: PerformanceReport) -> str:
|
|
|
441
472
|
lines.append("")
|
|
442
473
|
|
|
443
474
|
lines.append("## Trend signals")
|
|
444
|
-
|
|
475
|
+
trend_signals = _coerce_str_list(report.trend_signals)
|
|
476
|
+
if not trend_signals:
|
|
445
477
|
lines.append("_None._")
|
|
446
|
-
for sig in
|
|
478
|
+
for sig in trend_signals:
|
|
447
479
|
lines.append(f"- {sig}")
|
|
448
480
|
lines.append("")
|
|
449
481
|
|
|
@@ -472,9 +504,10 @@ def _render_markdown(report: PerformanceReport) -> str:
|
|
|
472
504
|
lines.append(
|
|
473
505
|
f"- **{r.target}** (conf {r.confidence:.2f}){stale_tag} — {r.rationale}"
|
|
474
506
|
)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
507
|
+
source_ids = _coerce_str_list(r.source_ids)
|
|
508
|
+
if source_ids:
|
|
509
|
+
lines.append(f" - sources: {', '.join(source_ids)}")
|
|
510
|
+
for ev in _coerce_str_list(r.evidence):
|
|
478
511
|
lines.append(f" - evidence: {ev}")
|
|
479
512
|
lines.append("")
|
|
480
513
|
return "\n".join(lines).rstrip() + "\n"
|
|
@@ -13,6 +13,7 @@ import random
|
|
|
13
13
|
import re
|
|
14
14
|
import shutil
|
|
15
15
|
import subprocess
|
|
16
|
+
import time
|
|
16
17
|
from contextlib import nullcontext as _nullcontext, suppress
|
|
17
18
|
from dataclasses import dataclass, field
|
|
18
19
|
from datetime import datetime, timedelta, timezone
|
|
@@ -675,6 +676,27 @@ class Atlas:
|
|
|
675
676
|
],
|
|
676
677
|
}
|
|
677
678
|
|
|
679
|
+
@staticmethod
|
|
680
|
+
def _build_kai_task(content_brief: dict[str, Any]) -> str:
|
|
681
|
+
"""Build Kai's task without turning absent evidence into requirements."""
|
|
682
|
+
has_issues = bool(content_brief.get("github_issues"))
|
|
683
|
+
has_source_files = bool(content_brief.get("source_files"))
|
|
684
|
+
issue_instruction = (
|
|
685
|
+
"Reference real GitHub issues from Sage's triage."
|
|
686
|
+
if has_issues
|
|
687
|
+
else "Avoid GitHub issue claims unless Sage supplied real issue evidence."
|
|
688
|
+
)
|
|
689
|
+
source_instruction = (
|
|
690
|
+
"Use actual file paths, commands, and APIs from the source code."
|
|
691
|
+
if has_source_files
|
|
692
|
+
else "Avoid source-code and file-path claims unless Dex supplied source evidence."
|
|
693
|
+
)
|
|
694
|
+
return (
|
|
695
|
+
"Write a technical tutorial addressing the #1 developer pain point. "
|
|
696
|
+
"Ground the content in the knowledge base and Dex's architecture analysis. "
|
|
697
|
+
f"{issue_instruction} {source_instruction}"
|
|
698
|
+
)
|
|
699
|
+
|
|
678
700
|
def _slug(self, value: str, fallback: str) -> str:
|
|
679
701
|
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
|
680
702
|
return slug[:80] or fallback
|
|
@@ -734,13 +756,38 @@ class Atlas:
|
|
|
734
756
|
the last completed checkpoint instead of re-running everything.
|
|
735
757
|
Produces a run report with timing, cost, and quality data.
|
|
736
758
|
"""
|
|
737
|
-
from devrel_origin.tools.run_report import RunReport
|
|
759
|
+
from devrel_origin.tools.run_report import AgentTiming, RunReport
|
|
738
760
|
|
|
739
761
|
run_report = RunReport(
|
|
740
762
|
week_of=self.context.week_of,
|
|
741
763
|
started_at=datetime.now().isoformat(),
|
|
742
764
|
)
|
|
743
765
|
|
|
766
|
+
async def timed_delegate(
|
|
767
|
+
stage: int,
|
|
768
|
+
agent_name: str,
|
|
769
|
+
task: str,
|
|
770
|
+
context: Optional[dict[str, Any]] = None,
|
|
771
|
+
) -> DelegationResult:
|
|
772
|
+
started_at = datetime.now(timezone.utc)
|
|
773
|
+
t0 = time.monotonic()
|
|
774
|
+
result = await self.delegate(agent_name, task, context)
|
|
775
|
+
completed_at = datetime.now(timezone.utc)
|
|
776
|
+
run_report.agent_timings.append(
|
|
777
|
+
AgentTiming(
|
|
778
|
+
agent=agent_name,
|
|
779
|
+
stage=stage,
|
|
780
|
+
started_at=started_at.isoformat(),
|
|
781
|
+
completed_at=completed_at.isoformat(),
|
|
782
|
+
duration_seconds=time.monotonic() - t0,
|
|
783
|
+
success=result.success,
|
|
784
|
+
error=result.error or "",
|
|
785
|
+
)
|
|
786
|
+
)
|
|
787
|
+
if not result.success and result.error:
|
|
788
|
+
run_report.errors.append(f"{agent_name}: {result.error}")
|
|
789
|
+
return result
|
|
790
|
+
|
|
744
791
|
# Check for existing checkpoint to resume from
|
|
745
792
|
checkpoint = self._load_checkpoint(self.archive_dir, self.context.week_of)
|
|
746
793
|
resume_stage = 0
|
|
@@ -765,7 +812,8 @@ class Atlas:
|
|
|
765
812
|
|
|
766
813
|
# Stage 0: Watchdog health check (pre-flight)
|
|
767
814
|
if resume_stage <= 0 and "watchdog" not in completed_agents:
|
|
768
|
-
watchdog_result = await
|
|
815
|
+
watchdog_result = await timed_delegate(
|
|
816
|
+
0,
|
|
769
817
|
"watchdog",
|
|
770
818
|
"Run system health check. Verify all integrations are "
|
|
771
819
|
"reachable and check for stale agent outputs from last cycle.",
|
|
@@ -793,7 +841,7 @@ class Atlas:
|
|
|
793
841
|
"Produce an architecture overview and API reference."
|
|
794
842
|
),
|
|
795
843
|
}
|
|
796
|
-
coros = [
|
|
844
|
+
coros = [timed_delegate(1, a, tasks_1[a]) for a in stage_1_pending]
|
|
797
845
|
results = await asyncio.gather(*coros)
|
|
798
846
|
for agent_name, res in zip(stage_1_pending, results, strict=True):
|
|
799
847
|
if res.success:
|
|
@@ -822,7 +870,7 @@ class Atlas:
|
|
|
822
870
|
"severity."
|
|
823
871
|
),
|
|
824
872
|
}
|
|
825
|
-
coros = [
|
|
873
|
+
coros = [timed_delegate(2, a, tasks_2[a]) for a in stage_2_pending]
|
|
826
874
|
results = await asyncio.gather(*coros)
|
|
827
875
|
for agent_name, res in zip(stage_2_pending, results, strict=True):
|
|
828
876
|
if res.success:
|
|
@@ -838,21 +886,17 @@ class Atlas:
|
|
|
838
886
|
stage_3_agents = ["nova", "kai"]
|
|
839
887
|
stage_3_pending = [a for a in stage_3_agents if a not in completed_agents]
|
|
840
888
|
if stage_3_pending:
|
|
889
|
+
content_brief = self._build_content_brief()
|
|
841
890
|
tasks_3 = {
|
|
842
891
|
"nova": (
|
|
843
892
|
"Design activation experiments based on the top pain points. "
|
|
844
893
|
"Include pre-registration, power analysis, and success criteria."
|
|
845
894
|
),
|
|
846
|
-
"kai": (
|
|
847
|
-
"Write a technical tutorial addressing the #1 developer pain point. "
|
|
848
|
-
"Ground the content in the knowledge base and Dex's architecture "
|
|
849
|
-
"analysis. Reference real GitHub issues from Sage's triage. "
|
|
850
|
-
"Use actual file paths, commands, and APIs from the source code."
|
|
851
|
-
),
|
|
895
|
+
"kai": self._build_kai_task(content_brief),
|
|
852
896
|
}
|
|
853
|
-
content_brief = self._build_content_brief()
|
|
854
897
|
coros = [
|
|
855
|
-
|
|
898
|
+
timed_delegate(
|
|
899
|
+
3,
|
|
856
900
|
a,
|
|
857
901
|
tasks_3[a],
|
|
858
902
|
context={"content_brief": content_brief} if a == "kai" else None,
|
|
@@ -871,7 +915,8 @@ class Atlas:
|
|
|
871
915
|
|
|
872
916
|
# Stage 4: Vox (uses Kai's content)
|
|
873
917
|
if resume_stage <= 4 and "vox" not in completed_agents:
|
|
874
|
-
video_result = await
|
|
918
|
+
video_result = await timed_delegate(
|
|
919
|
+
4,
|
|
875
920
|
"vox",
|
|
876
921
|
"Generate a video tutorial from Kai's written content. "
|
|
877
922
|
"Record screen walkthrough with narration and overlays.",
|
|
@@ -883,7 +928,8 @@ class Atlas:
|
|
|
883
928
|
|
|
884
929
|
# Stage 5: Sentinel brand audit — audit all generated content
|
|
885
930
|
if resume_stage <= 5 and "sentinel" not in completed_agents:
|
|
886
|
-
sentinel_result = await
|
|
931
|
+
sentinel_result = await timed_delegate(
|
|
932
|
+
5,
|
|
887
933
|
"sentinel",
|
|
888
934
|
"Audit all generated content for brand voice consistency, "
|
|
889
935
|
"ICP alignment, messaging coherence, and technical accuracy.",
|
|
@@ -44,6 +44,40 @@ def _check_playwright() -> bool:
|
|
|
44
44
|
return False
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def _missing_video_dependencies(
|
|
48
|
+
*,
|
|
49
|
+
has_ffmpeg: bool,
|
|
50
|
+
has_playwright: bool,
|
|
51
|
+
has_openai_key: bool,
|
|
52
|
+
) -> list[dict[str, str]]:
|
|
53
|
+
missing: list[dict[str, str]] = []
|
|
54
|
+
if not has_ffmpeg:
|
|
55
|
+
missing.append(
|
|
56
|
+
{
|
|
57
|
+
"name": "ffmpeg",
|
|
58
|
+
"fix": "Install FFmpeg, for example `brew install ffmpeg` on macOS.",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
if not has_playwright:
|
|
62
|
+
missing.append(
|
|
63
|
+
{
|
|
64
|
+
"name": "playwright",
|
|
65
|
+
"fix": (
|
|
66
|
+
"Install video extras and browsers: "
|
|
67
|
+
"`pip install 'devrel-origin[video]' && python -m playwright install chromium`."
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
if not has_openai_key:
|
|
72
|
+
missing.append(
|
|
73
|
+
{
|
|
74
|
+
"name": "OPENAI_API_KEY",
|
|
75
|
+
"fix": "Set `OPENAI_API_KEY` or run `devrel auth` before rendering video.",
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
return missing
|
|
79
|
+
|
|
80
|
+
|
|
47
81
|
class Vox:
|
|
48
82
|
"""
|
|
49
83
|
Video Tutorial agent that produces screen-recorded tutorials.
|
|
@@ -94,7 +128,11 @@ Keep narration concise and developer-focused. Show, don't tell."""
|
|
|
94
128
|
if not self._has_ffmpeg:
|
|
95
129
|
logger.warning("FFmpeg not found — video rendering will be skipped")
|
|
96
130
|
if not self._has_playwright:
|
|
97
|
-
logger.warning(
|
|
131
|
+
logger.warning(
|
|
132
|
+
"Playwright not installed — recording will be skipped. "
|
|
133
|
+
"Install video extras and browsers: `pip install 'devrel-origin[video]' "
|
|
134
|
+
"&& python -m playwright install chromium`."
|
|
135
|
+
)
|
|
98
136
|
|
|
99
137
|
async def execute(
|
|
100
138
|
self,
|
|
@@ -170,17 +208,29 @@ Keep narration concise and developer-focused. Show, don't tell."""
|
|
|
170
208
|
"status": "script_only",
|
|
171
209
|
}
|
|
172
210
|
|
|
173
|
-
|
|
211
|
+
missing_dependencies = _missing_video_dependencies(
|
|
212
|
+
has_ffmpeg=self._has_ffmpeg,
|
|
213
|
+
has_playwright=self._has_playwright,
|
|
214
|
+
has_openai_key=bool(self.openai_api_key),
|
|
215
|
+
)
|
|
216
|
+
result["video_produced"] = False
|
|
217
|
+
result["recording_skipped"] = bool(missing_dependencies)
|
|
218
|
+
result["missing_dependencies"] = missing_dependencies
|
|
219
|
+
|
|
220
|
+
can_render = not missing_dependencies
|
|
174
221
|
if can_render:
|
|
175
222
|
try:
|
|
176
223
|
output_path = await self._run_full_pipeline(tutorial)
|
|
177
224
|
result["status"] = "generated"
|
|
178
225
|
result["output_path"] = str(output_path)
|
|
179
226
|
result["total_duration"] = tutorial.total_duration
|
|
227
|
+
result["video_produced"] = True
|
|
228
|
+
result["recording_skipped"] = False
|
|
180
229
|
except Exception as exc:
|
|
181
230
|
logger.error(f"Video pipeline failed: {exc}")
|
|
182
231
|
result["status"] = "script_only"
|
|
183
232
|
result["pipeline_error"] = str(exc)
|
|
233
|
+
result["recording_skipped"] = True
|
|
184
234
|
|
|
185
235
|
return result
|
|
186
236
|
|
|
@@ -15,7 +15,7 @@ from contextlib import contextmanager
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Iterator
|
|
17
17
|
|
|
18
|
-
SCHEMA_VERSION =
|
|
18
|
+
SCHEMA_VERSION = 6
|
|
19
19
|
|
|
20
20
|
SCHEMA = """
|
|
21
21
|
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
@@ -159,9 +159,56 @@ CREATE TABLE IF NOT EXISTS cro_funnel_metrics (
|
|
|
159
159
|
|
|
160
160
|
CREATE INDEX IF NOT EXISTS idx_cro_funnel_period
|
|
161
161
|
ON cro_funnel_metrics(funnel_id, period_end DESC);
|
|
162
|
+
|
|
163
|
+
CREATE TABLE IF NOT EXISTS social_mentions (
|
|
164
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
165
|
+
platform TEXT NOT NULL,
|
|
166
|
+
post_id TEXT NOT NULL,
|
|
167
|
+
title TEXT,
|
|
168
|
+
url TEXT,
|
|
169
|
+
author TEXT,
|
|
170
|
+
content TEXT,
|
|
171
|
+
sentiment TEXT,
|
|
172
|
+
posted_at TEXT NOT NULL,
|
|
173
|
+
subreddit TEXT,
|
|
174
|
+
upvotes INTEGER NOT NULL DEFAULT 0,
|
|
175
|
+
comments INTEGER NOT NULL DEFAULT 0,
|
|
176
|
+
engagement_score REAL NOT NULL DEFAULT 0,
|
|
177
|
+
is_own_post INTEGER NOT NULL DEFAULT 0,
|
|
178
|
+
is_question INTEGER NOT NULL DEFAULT 0,
|
|
179
|
+
requires_response INTEGER NOT NULL DEFAULT 0,
|
|
180
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
181
|
+
UNIQUE (platform, post_id)
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_social_mentions_posted_at
|
|
185
|
+
ON social_mentions(posted_at DESC);
|
|
186
|
+
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_social_mentions_own_period
|
|
188
|
+
ON social_mentions(is_own_post, posted_at DESC);
|
|
162
189
|
"""
|
|
163
190
|
|
|
164
191
|
|
|
192
|
+
_SOCIAL_MENTIONS_COLUMNS: dict[str, str] = {
|
|
193
|
+
"platform": "TEXT NOT NULL DEFAULT ''",
|
|
194
|
+
"post_id": "TEXT NOT NULL DEFAULT ''",
|
|
195
|
+
"title": "TEXT",
|
|
196
|
+
"url": "TEXT",
|
|
197
|
+
"author": "TEXT",
|
|
198
|
+
"content": "TEXT",
|
|
199
|
+
"sentiment": "TEXT",
|
|
200
|
+
"posted_at": "TEXT NOT NULL DEFAULT ''",
|
|
201
|
+
"subreddit": "TEXT",
|
|
202
|
+
"upvotes": "INTEGER NOT NULL DEFAULT 0",
|
|
203
|
+
"comments": "INTEGER NOT NULL DEFAULT 0",
|
|
204
|
+
"engagement_score": "REAL NOT NULL DEFAULT 0",
|
|
205
|
+
"is_own_post": "INTEGER NOT NULL DEFAULT 0",
|
|
206
|
+
"is_question": "INTEGER NOT NULL DEFAULT 0",
|
|
207
|
+
"requires_response": "INTEGER NOT NULL DEFAULT 0",
|
|
208
|
+
"created_at": "TEXT NOT NULL DEFAULT ''",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
165
212
|
def _migrate_to_v5(conn: sqlite3.Connection) -> None:
|
|
166
213
|
"""Add pillar + target_kind columns to analytics_recommendations if absent.
|
|
167
214
|
|
|
@@ -200,6 +247,67 @@ def _migrate_to_v5(conn: sqlite3.Connection) -> None:
|
|
|
200
247
|
)
|
|
201
248
|
|
|
202
249
|
|
|
250
|
+
def _migrate_to_v6(conn: sqlite3.Connection) -> None:
|
|
251
|
+
"""Ensure Echo/Argus social mention storage exists and has v6 columns."""
|
|
252
|
+
conn.execute(
|
|
253
|
+
"""
|
|
254
|
+
CREATE TABLE IF NOT EXISTS social_mentions (
|
|
255
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
256
|
+
platform TEXT NOT NULL,
|
|
257
|
+
post_id TEXT NOT NULL,
|
|
258
|
+
title TEXT,
|
|
259
|
+
url TEXT,
|
|
260
|
+
author TEXT,
|
|
261
|
+
content TEXT,
|
|
262
|
+
sentiment TEXT,
|
|
263
|
+
posted_at TEXT NOT NULL,
|
|
264
|
+
subreddit TEXT,
|
|
265
|
+
upvotes INTEGER NOT NULL DEFAULT 0,
|
|
266
|
+
comments INTEGER NOT NULL DEFAULT 0,
|
|
267
|
+
engagement_score REAL NOT NULL DEFAULT 0,
|
|
268
|
+
is_own_post INTEGER NOT NULL DEFAULT 0,
|
|
269
|
+
is_question INTEGER NOT NULL DEFAULT 0,
|
|
270
|
+
requires_response INTEGER NOT NULL DEFAULT 0,
|
|
271
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
272
|
+
UNIQUE (platform, post_id)
|
|
273
|
+
)
|
|
274
|
+
"""
|
|
275
|
+
)
|
|
276
|
+
cur = conn.execute("PRAGMA table_info(social_mentions)")
|
|
277
|
+
cols = {row[1] for row in cur.fetchall()}
|
|
278
|
+
for col, ddl in _SOCIAL_MENTIONS_COLUMNS.items():
|
|
279
|
+
if col not in cols:
|
|
280
|
+
conn.execute(f"ALTER TABLE social_mentions ADD COLUMN {col} {ddl}")
|
|
281
|
+
|
|
282
|
+
cols = {row[1] for row in conn.execute("PRAGMA table_info(social_mentions)").fetchall()}
|
|
283
|
+
if "score" in cols:
|
|
284
|
+
conn.execute(
|
|
285
|
+
"UPDATE social_mentions "
|
|
286
|
+
"SET engagement_score = COALESCE(NULLIF(engagement_score, 0), score, 0)"
|
|
287
|
+
)
|
|
288
|
+
if "engagement" in cols:
|
|
289
|
+
conn.execute(
|
|
290
|
+
"UPDATE social_mentions "
|
|
291
|
+
"SET engagement_score = COALESCE(NULLIF(engagement_score, 0), engagement, 0), "
|
|
292
|
+
" upvotes = COALESCE(NULLIF(upvotes, 0), engagement, 0)"
|
|
293
|
+
)
|
|
294
|
+
if "url" in cols:
|
|
295
|
+
fallback_id = "CAST(id AS TEXT)" if "id" in cols else "rowid"
|
|
296
|
+
conn.execute(
|
|
297
|
+
"UPDATE social_mentions "
|
|
298
|
+
f"SET post_id = COALESCE(NULLIF(post_id, ''), NULLIF(url, ''), {fallback_id}) "
|
|
299
|
+
"WHERE post_id IS NULL OR post_id = ''"
|
|
300
|
+
)
|
|
301
|
+
conn.execute(
|
|
302
|
+
"CREATE INDEX IF NOT EXISTS idx_social_mentions_posted_at "
|
|
303
|
+
"ON social_mentions(posted_at DESC)"
|
|
304
|
+
)
|
|
305
|
+
conn.execute(
|
|
306
|
+
"CREATE INDEX IF NOT EXISTS idx_social_mentions_own_period "
|
|
307
|
+
"ON social_mentions(is_own_post, posted_at DESC)"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
203
311
|
def init_db(db_path: Path) -> None:
|
|
204
312
|
"""Create the DB file and apply the schema. Idempotent: preserves
|
|
205
313
|
existing data and bumps schema_meta to the current SCHEMA_VERSION."""
|
|
@@ -207,6 +315,7 @@ def init_db(db_path: Path) -> None:
|
|
|
207
315
|
with sqlite3.connect(db_path) as conn:
|
|
208
316
|
conn.executescript(SCHEMA)
|
|
209
317
|
_migrate_to_v5(conn)
|
|
318
|
+
_migrate_to_v6(conn)
|
|
210
319
|
conn.execute(
|
|
211
320
|
"INSERT OR REPLACE INTO schema_meta (version, applied_at) VALUES (?, datetime('now'))",
|
|
212
321
|
(SCHEMA_VERSION,),
|
|
@@ -10,7 +10,7 @@ Stage flow:
|
|
|
10
10
|
6. Persona — Haiku score 1-10 + weak sections
|
|
11
11
|
7. Readability — pure-Python FRE/sentence-stats/jargon check
|
|
12
12
|
→ If 6 or 7 fail: re-run stage 4 once with the failed rubric, then
|
|
13
|
-
re-run 5/6/7 once. Second failure
|
|
13
|
+
re-run 5/6/7 once. Second persona failure aborts loudly.
|
|
14
14
|
8. Brand audit — Sentinel (caller's responsibility; orchestrator
|
|
15
15
|
does not invoke Sentinel because it lives in
|
|
16
16
|
core/sentinel.py and would create a quality→core
|
|
@@ -328,17 +328,16 @@ async def run_pipeline(
|
|
|
328
328
|
readability2 = _readability_stage(text=text, content_type=content_type, style_md=style_md)
|
|
329
329
|
stages.append(readability2)
|
|
330
330
|
|
|
331
|
-
# Readability re-runs are informational only
|
|
332
|
-
# often fails MSL
|
|
333
|
-
# Only persona2 failure flips the flagged bit.
|
|
331
|
+
# Readability re-runs are informational only because short test/mock
|
|
332
|
+
# text often fails MSL. Persona is the hard ship/no-ship gate.
|
|
334
333
|
if persona2.issues:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
"
|
|
334
|
+
issue_text = "; ".join(persona2.issues)
|
|
335
|
+
logger.error(
|
|
336
|
+
"editorial pipeline aborting for content_type=%s after persona repair failed: %s",
|
|
338
337
|
content_type,
|
|
339
|
-
|
|
338
|
+
issue_text,
|
|
340
339
|
)
|
|
341
|
-
|
|
340
|
+
raise AbortLoud(f"Persona gate failed after repair for {content_type}: {issue_text}")
|
|
342
341
|
|
|
343
342
|
revision_trace = {
|
|
344
343
|
"content_type": content_type,
|
|
@@ -54,14 +54,17 @@ class PostHogCollector:
|
|
|
54
54
|
|
|
55
55
|
def __init__(self, client: "PostHogClient"):
|
|
56
56
|
self.client = client
|
|
57
|
+
self.last_ok = True
|
|
57
58
|
|
|
58
59
|
async def collect(self, period: Period) -> list[PerformanceMetric]:
|
|
59
60
|
_start, end = period
|
|
60
61
|
try:
|
|
61
62
|
rows = await self.client.fetch_events_by_url(start=_start, end=end)
|
|
62
63
|
except Exception as exc: # noqa: BLE001
|
|
64
|
+
self.last_ok = False
|
|
63
65
|
logger.warning("PostHogCollector failed: %s", exc)
|
|
64
66
|
return []
|
|
67
|
+
self.last_ok = True
|
|
65
68
|
|
|
66
69
|
metrics: list[PerformanceMetric] = []
|
|
67
70
|
for row in rows:
|
|
@@ -95,14 +98,19 @@ class GitHubCollector:
|
|
|
95
98
|
|
|
96
99
|
def __init__(self, client):
|
|
97
100
|
self.client = client
|
|
101
|
+
self.last_ok = True
|
|
98
102
|
|
|
99
103
|
async def collect(self, period: Period) -> list[PerformanceMetric]:
|
|
100
104
|
_start, end = period
|
|
101
105
|
try:
|
|
102
106
|
stats = await self.client.get_repo_stats()
|
|
107
|
+
if not isinstance(stats, dict):
|
|
108
|
+
raise TypeError("github stats payload is not a dict")
|
|
103
109
|
except Exception as exc: # noqa: BLE001
|
|
110
|
+
self.last_ok = False
|
|
104
111
|
logger.warning("GitHubCollector failed: %s", exc)
|
|
105
112
|
return []
|
|
113
|
+
self.last_ok = True
|
|
106
114
|
|
|
107
115
|
repo = getattr(self.client, "repo_full_name", "unknown/unknown")
|
|
108
116
|
return [
|
|
@@ -152,14 +160,22 @@ class InstantlyCollector:
|
|
|
152
160
|
|
|
153
161
|
def __init__(self, client):
|
|
154
162
|
self.client = client
|
|
163
|
+
self.last_ok = True
|
|
155
164
|
|
|
156
165
|
async def collect(self, period: Period) -> list[PerformanceMetric]:
|
|
166
|
+
if self.client is None:
|
|
167
|
+
self.last_ok = False
|
|
168
|
+
logger.info("InstantlyCollector: client not configured, skipping")
|
|
169
|
+
return []
|
|
170
|
+
|
|
157
171
|
start, end = period
|
|
158
172
|
try:
|
|
159
173
|
rows = await self.client.list_campaigns_with_analytics()
|
|
160
174
|
except Exception as exc: # noqa: BLE001
|
|
175
|
+
self.last_ok = False
|
|
161
176
|
logger.warning("InstantlyCollector failed: %s", exc)
|
|
162
177
|
return []
|
|
178
|
+
self.last_ok = True
|
|
163
179
|
|
|
164
180
|
metrics: list[PerformanceMetric] = []
|
|
165
181
|
for row in rows:
|
|
@@ -218,6 +234,7 @@ class SocialCollector:
|
|
|
218
234
|
def __init__(self, state_db_path: Path):
|
|
219
235
|
self.state_db_path = state_db_path
|
|
220
236
|
self._schema_verified = False
|
|
237
|
+
self.last_ok = True
|
|
221
238
|
|
|
222
239
|
def _verify_schema(self, conn: sqlite3.Connection) -> bool:
|
|
223
240
|
"""Confirm social_mentions has all required columns.
|
|
@@ -269,6 +286,7 @@ class SocialCollector:
|
|
|
269
286
|
async def collect(self, period: Period) -> list[PerformanceMetric]:
|
|
270
287
|
start, end = period
|
|
271
288
|
if not self.state_db_path.is_file():
|
|
289
|
+
self.last_ok = False
|
|
272
290
|
logger.info("SocialCollector: state.db not present, skipping")
|
|
273
291
|
return []
|
|
274
292
|
|
|
@@ -278,7 +296,9 @@ class SocialCollector:
|
|
|
278
296
|
end.isoformat(),
|
|
279
297
|
)
|
|
280
298
|
if rows is None:
|
|
299
|
+
self.last_ok = False
|
|
281
300
|
return []
|
|
301
|
+
self.last_ok = True
|
|
282
302
|
|
|
283
303
|
metrics: list[PerformanceMetric] = []
|
|
284
304
|
for row in rows:
|