arkaos 5.17.2 → 5.18.0
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.
- package/THE-ARKAOS-GUIDE.md +2 -2
- package/VERSION +1 -1
- package/arka/SKILL.md +2 -1
- package/arka/skills/costs/SKILL.md +16 -0
- package/config/claude-agents/marta-cqo.md +10 -0
- package/config/decisions/corpora/bash-effect.jsonl +38 -0
- package/config/decisions/corpora/creation-intent.jsonl +47 -0
- package/config/decisions/corpora/dispatch-role.jsonl +32 -0
- package/config/decisions/corpora/forge-complexity.jsonl +102 -0
- package/config/decisions/corpora/forge-departments.jsonl +34 -0
- package/config/decisions/corpora/heldout/learning-signal.jsonl +17 -0
- package/config/decisions/corpora/heldout/qg-prescreen.jsonl +20 -0
- package/config/decisions/corpora/heldout/slop-score.jsonl +20 -0
- package/config/decisions/corpora/learning-signal.jsonl +35 -0
- package/config/decisions/corpora/phantom-action.jsonl +34 -0
- package/config/decisions/corpora/qg-prescreen.jsonl +46 -0
- package/config/decisions/corpora/refine.jsonl +104 -0
- package/config/decisions/corpora/route.jsonl +33 -0
- package/config/decisions/corpora/skill-hints.jsonl +34 -0
- package/config/decisions/corpora/skill-proposer.jsonl +32 -0
- package/config/decisions/corpora/slop-score.jsonl +33 -0
- package/config/decisions/corpora/subagent-discipline.jsonl +32 -0
- package/config/decisions/corpora/sycophancy.jsonl +34 -0
- package/config/decisions/corpora/topic-drift.jsonl +34 -0
- package/config/decisions/corpora/ui-in-ts.jsonl +60 -0
- package/config/hooks/gate-manifest.json +1 -0
- package/config/models.yaml +10 -2
- package/core/decisions/__init__.py +8 -0
- package/core/decisions/backoff.py +137 -0
- package/core/decisions/cache.py +92 -0
- package/core/decisions/client.py +191 -0
- package/core/decisions/config.py +159 -0
- package/core/decisions/cost_effect.py +242 -0
- package/core/decisions/engine.py +400 -0
- package/core/decisions/models.py +118 -0
- package/core/decisions/paths.py +39 -0
- package/core/decisions/privacy.py +378 -0
- package/core/decisions/promotion.py +126 -0
- package/core/decisions/registry.py +23 -0
- package/core/decisions/replay.py +852 -0
- package/core/decisions/replay_report.py +413 -0
- package/core/decisions/shadow.py +147 -0
- package/core/decisions/site.py +343 -0
- package/core/decisions/sites/__init__.py +1 -0
- package/core/decisions/sites/command.py +58 -0
- package/core/decisions/sites/dispatch.py +161 -0
- package/core/decisions/sites/forge.py +230 -0
- package/core/decisions/sites/governance.py +361 -0
- package/core/decisions/sites/prompt.py +195 -0
- package/core/decisions/sites/quality.py +281 -0
- package/core/decisions/telemetry.py +335 -0
- package/core/decisions/telemetry_cli.py +149 -0
- package/core/decisions/transport.py +97 -0
- package/core/egress/audit.py +46 -6
- package/core/egress/credentials.py +1296 -0
- package/core/egress/policy.py +7 -4
- package/core/evals/verdict_labels.py +25 -5
- package/core/forge/budget.py +32 -0
- package/core/forge/complexity.py +11 -3
- package/core/forge/jev_sites.py +80 -0
- package/core/forge/orchestrator.py +76 -72
- package/core/governance/aggregate_guard.py +175 -77
- package/core/governance/carry_advisor.py +36 -19
- package/core/governance/evidence_checks.py +134 -55
- package/core/governance/fence_recovery.py +165 -0
- package/core/governance/harness_scanner.py +4 -0
- package/core/governance/jev_advisory.py +154 -0
- package/core/governance/leak_scanner.py +2 -2
- package/core/governance/literal_git.py +66 -0
- package/core/governance/qg_prescreen.py +518 -0
- package/core/governance/qg_tier.py +17 -0
- package/core/governance/reviewer_ledger.py +225 -46
- package/core/governance/skill_proposer.py +38 -13
- package/core/governance/slop_check.py +169 -0
- package/core/hooks/operator_message.py +128 -0
- package/core/hooks/stop.py +418 -38
- package/core/hooks/stop_budget.py +43 -0
- package/core/hooks/subagent_stop.py +39 -9
- package/core/hooks/ups_dispatch.py +243 -0
- package/core/hooks/user_prompt_submit.py +458 -106
- package/core/keys.py +4 -0
- package/core/runtime/model_router.py +38 -0
- package/core/runtime/pricing.py +8 -0
- package/core/synapse/command_menu.py +70 -0
- package/core/synapse/layers.py +108 -66
- package/core/workflow/flow_enforcer.py +71 -1
- package/core/workflow/frontend_gate.py +57 -1
- package/core/workflow/transcript_scope.py +34 -25
- package/departments/quality/SKILL.md +29 -2
- package/harness/codex/AGENTS.md +1 -1
- package/harness/copilot/copilot-instructions.md +1 -1
- package/harness/cursor/rules/arkaos.mdc +2 -2
- package/harness/gemini/GEMINI.md +1 -1
- package/harness/opencode/AGENTS.md +1 -1
- package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
- package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
- package/harness/opencode/agents/arka-cfo-helena.md +1 -1
- package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
- package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
- package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
- package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
- package/harness/opencode/agents/arka-coo-sofia.md +1 -1
- package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
- package/harness/opencode/agents/arka-cqo-marta.md +1 -1
- package/harness/opencode/agents/arka-cto-marco.md +1 -1
- package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
- package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
- package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
- package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
- package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
- package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
- package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
- package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
- package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
- package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
- package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
- package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
- package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
- package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
- package/harness/zed/.rules +1 -1
- package/installer/config-seed.js +71 -1
- package/installer/keys.js +1 -1
- package/installer/update.js +29 -0
- package/knowledge/commands-registry.json +17 -4
- package/knowledge/commands-registry.json.bak +6739 -0
- package/knowledge/skills-manifest.json +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/synapse-bridge.py +17 -1
- package/scripts/tools/corpus_merge.py +402 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Replay report — N measured runs per site, the D2 proposal, one evidence folder.
|
|
2
|
+
|
|
3
|
+
``arka-py -m core.decisions.replay_report (--all | --sites a,b) --session <id>
|
|
4
|
+
[--runs 2] [--live-window 7d]`` (spec PR5, decision D1).
|
|
5
|
+
|
|
6
|
+
* Each run is :func:`core.decisions.replay.replay` at the site's REAL
|
|
7
|
+
ceiling (``site_timeout_ms``; there is no ``--timeout-ms``) on a config
|
|
8
|
+
copy with ``cacheTtlSeconds=0``: ``cache.get`` misses by construction,
|
|
9
|
+
so run 2 asks the endpoint again instead of reading run 1's answers.
|
|
10
|
+
* A run is **measured** only when ≥ :data:`MIN_REACHED_SHARE` of the
|
|
11
|
+
cases that asked a question reached the endpoint and ≤
|
|
12
|
+
:data:`MAX_UNAVAILABLE_SHARE` of all cases were unavailable. Otherwise
|
|
13
|
+
it is ``not-measured``: it moves no mode and the CLI exits 1.
|
|
14
|
+
* The gate per run is the replay gate (Jev ≥ heuristic + 5 pp on the
|
|
15
|
+
answered cases, abstain ≤ 25 %, false escalation ≤ 5 %, MAE ≤ 5.0 for
|
|
16
|
+
slop-score). :func:`core.decisions.promotion.propose_mode` turns the
|
|
17
|
+
runs into a proposal against ``Site.default_mode``; nothing here edits
|
|
18
|
+
config or code.
|
|
19
|
+
* Replay calls land in the cost ledger as ``decision-replay``.
|
|
20
|
+
* The ``live`` column (``telemetry.summarise`` over ``--live-window``) is
|
|
21
|
+
informational: it never decides a mode.
|
|
22
|
+
|
|
23
|
+
Writes ``~/.arkaos/quality-gate/<session>/replay-final/``:
|
|
24
|
+
``<site>-run<k>.json``, ``REPORT.md``, ``SUMMARY.json``, ``PROPOSALS.json``.
|
|
25
|
+
Exit codes: 0 every site measured, 1 some run not measured, 2 usage /
|
|
26
|
+
bypass / no transport / bad corpus.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import hashlib
|
|
33
|
+
import json
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
from collections.abc import Sequence
|
|
37
|
+
from dataclasses import asdict, dataclass, field
|
|
38
|
+
from datetime import UTC, datetime
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any
|
|
41
|
+
|
|
42
|
+
from core.decisions.config import (
|
|
43
|
+
DecisionsConfig,
|
|
44
|
+
Mode,
|
|
45
|
+
bypassed,
|
|
46
|
+
configured_mode,
|
|
47
|
+
load_decisions_config,
|
|
48
|
+
)
|
|
49
|
+
from core.decisions.promotion import ModeProposal, RunGate, RunVerdict, propose_mode
|
|
50
|
+
from core.decisions.registry import SITES
|
|
51
|
+
from core.decisions.replay import ReplayCase, ReplayReport, default_corpus_path, load_corpus, replay
|
|
52
|
+
from core.decisions.telemetry import REPLAY_COST_CATEGORY, SiteSummary, summarise
|
|
53
|
+
from core.decisions.transport import Transport, resolve_transport
|
|
54
|
+
|
|
55
|
+
MIN_REACHED_SHARE = 0.90
|
|
56
|
+
MAX_UNAVAILABLE_SHARE = 0.10
|
|
57
|
+
OUT_DIRNAME = "replay-final"
|
|
58
|
+
SESSION_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}")
|
|
59
|
+
LIVE_WINDOWS: dict[str, str] = {
|
|
60
|
+
"today": "today", "7d": "week", "week": "week", "30d": "month", "month": "month",
|
|
61
|
+
"all": "all",
|
|
62
|
+
}
|
|
63
|
+
LIVE_DRIFT_ABSTAIN_PCT = 25.0
|
|
64
|
+
LIVE_DRIFT_MIN_CALLS = 50
|
|
65
|
+
LIVE_DRIFT_PERIODS = frozenset({"week", "month", "all"}) # windows of >= 7 days
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class SiteRun:
|
|
70
|
+
"""One replay run of one site and whether it counts."""
|
|
71
|
+
|
|
72
|
+
run: int
|
|
73
|
+
gate: RunGate
|
|
74
|
+
why_not_measured: str
|
|
75
|
+
report: ReplayReport
|
|
76
|
+
ts: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class LiveView:
|
|
81
|
+
"""Informational live telemetry for one site (never decides a mode)."""
|
|
82
|
+
|
|
83
|
+
calls: int
|
|
84
|
+
abstain_attributable_pct: float | None
|
|
85
|
+
fallback_pct: float
|
|
86
|
+
acted_jev_pct: float
|
|
87
|
+
top_fallback_reasons: list[tuple[str, int]]
|
|
88
|
+
live_drift: bool
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class SiteResult:
|
|
93
|
+
"""Every run of one site, its corpus digest, modes and the proposal."""
|
|
94
|
+
|
|
95
|
+
site: str
|
|
96
|
+
corpus_sha256: str
|
|
97
|
+
default_mode: Mode
|
|
98
|
+
effective_mode: Mode
|
|
99
|
+
runs: list[SiteRun]
|
|
100
|
+
proposal: ModeProposal
|
|
101
|
+
live: LiveView | None = None
|
|
102
|
+
cost_usd: float = 0.0
|
|
103
|
+
run_files: list[str] = field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class RunContext:
|
|
108
|
+
"""What every site's runs share."""
|
|
109
|
+
|
|
110
|
+
transport: Transport
|
|
111
|
+
cfg: DecisionsConfig
|
|
112
|
+
operator_cfg: DecisionsConfig
|
|
113
|
+
runs: int
|
|
114
|
+
out_dir: Path
|
|
115
|
+
live: dict[str, SiteSummary]
|
|
116
|
+
live_period: str
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _now_iso() -> str:
|
|
120
|
+
return datetime.now(UTC).isoformat()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def corpus_digest(site: str) -> str:
|
|
124
|
+
"""sha256 of the corpus file as shipped (a relabel changes it)."""
|
|
125
|
+
return hashlib.sha256(default_corpus_path(site).read_bytes()).hexdigest()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def no_cache_config(cfg: DecisionsConfig) -> DecisionsConfig:
|
|
129
|
+
"""``cfg`` with the answer cache off: every run asks the endpoint."""
|
|
130
|
+
return cfg.model_copy(update={"cache_ttl_seconds": 0})
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def effective_mode(cfg: DecisionsConfig, site: str) -> Mode:
|
|
134
|
+
"""The operator's override when it sets a mode, else the site default (bypass ignored)."""
|
|
135
|
+
return configured_mode(cfg, SITES[site])
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def measure(report: ReplayReport) -> tuple[RunGate, str]:
|
|
139
|
+
"""The run's gate, or ``not-measured`` with the reason."""
|
|
140
|
+
if report.asked is None or report.reached is None:
|
|
141
|
+
return "not-measured", "offline run"
|
|
142
|
+
if report.asked == 0:
|
|
143
|
+
return "not-measured", "no case asked a question"
|
|
144
|
+
reached = report.reached / report.asked
|
|
145
|
+
if reached < MIN_REACHED_SHARE:
|
|
146
|
+
return "not-measured", (f"{reached:.0%} of asked cases reached the endpoint "
|
|
147
|
+
f"(< {MIN_REACHED_SHARE:.0%})")
|
|
148
|
+
unavailable = report.unavailable / report.cases if report.cases else 1.0
|
|
149
|
+
if unavailable > MAX_UNAVAILABLE_SHARE:
|
|
150
|
+
return "not-measured", f"unavailable {unavailable:.0%} (> {MAX_UNAVAILABLE_SHARE:.0%})"
|
|
151
|
+
return ("pass" if report.gate == "pass" else "fail"), ""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def live_view(summary: SiteSummary | None, period: str) -> LiveView | None:
|
|
155
|
+
"""The live column for one site; ``live_drift`` flags a new run + corpus review."""
|
|
156
|
+
if summary is None:
|
|
157
|
+
return None
|
|
158
|
+
pct = summary.abstain_attributable_pct
|
|
159
|
+
drift = (period in LIVE_DRIFT_PERIODS and summary.calls >= LIVE_DRIFT_MIN_CALLS
|
|
160
|
+
and pct is not None and pct > LIVE_DRIFT_ABSTAIN_PCT)
|
|
161
|
+
return LiveView(summary.calls, pct, summary.fallback_pct, summary.acted_jev_pct,
|
|
162
|
+
list(summary.top_fallback_reasons), drift)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _write_json(path: Path, payload: object) -> None:
|
|
166
|
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2, default=str) + "\n",
|
|
167
|
+
encoding="utf-8")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _run_once(
|
|
171
|
+
site: str, k: int, cases: Sequence[ReplayCase], ctx: RunContext, digest: str
|
|
172
|
+
) -> SiteRun:
|
|
173
|
+
print(f"replay {site} run {k}/{ctx.runs} ({len(cases)} cases)", file=sys.stderr)
|
|
174
|
+
report = replay(site, cases, transport=ctx.transport, cfg=ctx.cfg)
|
|
175
|
+
gate, why = measure(report)
|
|
176
|
+
run = SiteRun(k, gate, why, report, _now_iso())
|
|
177
|
+
_write_json(ctx.out_dir / f"{site}-run{k}.json", {
|
|
178
|
+
**asdict(report), "run": k, "measured_gate": gate, "why_not_measured": why,
|
|
179
|
+
"corpus_sha256": digest, "cost_category": REPLAY_COST_CATEGORY, "ts": run.ts,
|
|
180
|
+
})
|
|
181
|
+
return run
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def run_site(site: str, cases: Sequence[ReplayCase], ctx: RunContext) -> SiteResult:
|
|
185
|
+
"""``ctx.runs`` sequential runs of one site, then the D2 proposal."""
|
|
186
|
+
digest = corpus_digest(site)
|
|
187
|
+
runs = [_run_once(site, k, cases, ctx, digest) for k in range(1, ctx.runs + 1)]
|
|
188
|
+
verdicts = [RunVerdict(r.gate, r.report.abstain_rate, digest, r.ts) for r in runs]
|
|
189
|
+
default = SITES[site].default_mode
|
|
190
|
+
return SiteResult(
|
|
191
|
+
site=site, corpus_sha256=digest, default_mode=default,
|
|
192
|
+
effective_mode=effective_mode(ctx.operator_cfg, site), runs=runs,
|
|
193
|
+
proposal=propose_mode(site, default, verdicts),
|
|
194
|
+
live=live_view(ctx.live.get(site), ctx.live_period),
|
|
195
|
+
cost_usd=round(sum(r.report.cost_usd or 0.0 for r in runs), 10),
|
|
196
|
+
run_files=[f"{site}-run{r.run}.json" for r in runs],
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# --- rendering ----------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _pct(value: float | None) -> str:
|
|
204
|
+
return "—" if value is None else f"{value:.1%}"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _num(value: object) -> str:
|
|
208
|
+
return "—" if value is None else str(value)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _run_row(result: SiteResult, run: SiteRun) -> str:
|
|
212
|
+
r = run.report
|
|
213
|
+
gate = run.gate if not run.why_not_measured else f"not-measured ({run.why_not_measured})"
|
|
214
|
+
return (
|
|
215
|
+
f"| `{result.site}` | {run.run} | {r.cases} | {r.pt_share:.0%} | "
|
|
216
|
+
f"{_pct(r.jev_accuracy)} vs {_pct(r.heuristic_on_answered)} | {_pct(r.abstain_rate)} | "
|
|
217
|
+
f"{_pct(r.false_escalation_rate)} | {_num(r.mean_abs_error)} | "
|
|
218
|
+
f"{_num(r.p50_latency_ms)} | {_num(r.timeout_ms)} | "
|
|
219
|
+
f"{_num(r.reached)}/{_num(r.asked)} | ${(r.cost_usd or 0.0):.6f} | {gate} |"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _mode_cell(result: SiteResult) -> str:
|
|
224
|
+
if result.effective_mode == result.default_mode:
|
|
225
|
+
return result.default_mode
|
|
226
|
+
return f"{result.default_mode} (operator override: {result.effective_mode})"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _proposal_row(result: SiteResult) -> str:
|
|
230
|
+
p = result.proposal
|
|
231
|
+
return (f"| `{result.site}` | {_mode_cell(result)} | {p.action} → {p.proposed} | "
|
|
232
|
+
f"{p.reason} | `{result.corpus_sha256[:16]}` |")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _live_row(result: SiteResult) -> str:
|
|
236
|
+
lv = result.live
|
|
237
|
+
if lv is None:
|
|
238
|
+
return f"| `{result.site}` | 0 | — | — | — | — | |"
|
|
239
|
+
reasons = ", ".join(f"{r} {n}" for r, n in lv.top_fallback_reasons) or "—"
|
|
240
|
+
drift = "live-drift" if lv.live_drift else ""
|
|
241
|
+
return (f"| `{result.site}` | {lv.calls} | {_pct_raw(lv.abstain_attributable_pct)} | "
|
|
242
|
+
f"{lv.fallback_pct:.1f}% | {lv.acted_jev_pct:.1f}% | {reasons} | {drift} |")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _pct_raw(value: float | None) -> str:
|
|
246
|
+
return "—" if value is None else f"{value:.1f}%"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _secondary_lines(results: Sequence[SiteResult]) -> list[str]:
|
|
250
|
+
"""Per-run scores outside the gate: D6's two readings, refine's gap choice."""
|
|
251
|
+
rows = [f"| `{res.site}` | {run.run} | {_pct(run.report.abstain_rate)} | "
|
|
252
|
+
f"{_pct(run.report.single_cell_abstain_rate)} | {_pct(run.report.gap_accuracy)} |"
|
|
253
|
+
for res in results for run in res.runs
|
|
254
|
+
if run.report.single_cell_abstain_rate is not None
|
|
255
|
+
or run.report.gap_accuracy is not None]
|
|
256
|
+
if not rows:
|
|
257
|
+
return []
|
|
258
|
+
return ["", "## Secondary scores (never part of the gate)", "",
|
|
259
|
+
"| Site | Run | Abstain (site reading) | Abstain (single-cell, pre-D6) | "
|
|
260
|
+
"Missing choice vs gap |", "|---|---|---|---|---|", *rows]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def render_report(results: Sequence[SiteResult], meta: dict[str, Any]) -> str:
|
|
264
|
+
"""REPORT.md: runs, proposals, then the informational live column."""
|
|
265
|
+
lines = [
|
|
266
|
+
f"# Replay report — session {meta['session']}", "",
|
|
267
|
+
f"- Generated: {meta['generated_at']}; runs per site: {meta['runs']}; "
|
|
268
|
+
f"answer cache: off (cacheTtlSeconds=0); ceiling: each site's timeout_ms",
|
|
269
|
+
f"- A run counts only when ≥ {MIN_REACHED_SHARE:.0%} of asked cases reached the "
|
|
270
|
+
f"endpoint and ≤ {MAX_UNAVAILABLE_SHARE:.0%} were unavailable",
|
|
271
|
+
f"- Replay cost: ${meta['cost_usd']:.6f} (ledger category `{REPLAY_COST_CATEGORY}`)",
|
|
272
|
+
"", "## Runs", "",
|
|
273
|
+
"| Site | Run | Cases | pt-PT | Jev vs heuristic (answered) | Abstain | False esc. | "
|
|
274
|
+
"MAE | p50 ms | Ceiling ms | Reached | Cost | Gate |",
|
|
275
|
+
"|---|---|---|---|---|---|---|---|---|---|---|---|---|",
|
|
276
|
+
]
|
|
277
|
+
lines += [_run_row(res, run) for res in results for run in res.runs]
|
|
278
|
+
lines += _secondary_lines(results)
|
|
279
|
+
lines += ["", "## Proposals (rule D2; the PR applies them to `default_mode`)", "",
|
|
280
|
+
"| Site | Mode (default) | Proposal | Why | Corpus sha256 |", "|---|---|---|---|---|"]
|
|
281
|
+
lines += [_proposal_row(res) for res in results]
|
|
282
|
+
lines += ["", f"## Live telemetry ({meta['live_window']}) — informational, never decides "
|
|
283
|
+
"a mode", "",
|
|
284
|
+
"| Site | Calls | Abstain (attributable) | Fallback | Acted Jev | Top fallback "
|
|
285
|
+
"reasons | Flag |", "|---|---|---|---|---|---|---|"]
|
|
286
|
+
lines += [_live_row(res) for res in results]
|
|
287
|
+
return "\n".join(lines) + "\n"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def summary_payload(results: Sequence[SiteResult], meta: dict[str, Any]) -> dict[str, Any]:
|
|
291
|
+
"""SUMMARY.json: what the ADR cites per site."""
|
|
292
|
+
sites = {
|
|
293
|
+
res.site: {
|
|
294
|
+
"corpus_sha256": res.corpus_sha256,
|
|
295
|
+
"default_mode": res.default_mode, "effective_mode": res.effective_mode,
|
|
296
|
+
"operator_override": res.effective_mode != res.default_mode,
|
|
297
|
+
"gates": [r.gate for r in res.runs],
|
|
298
|
+
"why_not_measured": [r.why_not_measured for r in res.runs],
|
|
299
|
+
"abstain": [r.report.abstain_rate for r in res.runs],
|
|
300
|
+
"proposal": asdict(res.proposal), "cost_usd": res.cost_usd,
|
|
301
|
+
"live": asdict(res.live) if res.live else None, "run_files": res.run_files,
|
|
302
|
+
}
|
|
303
|
+
for res in results
|
|
304
|
+
}
|
|
305
|
+
return {**meta, "sites": sites}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def write_outputs(results: Sequence[SiteResult], meta: dict[str, Any], out_dir: Path) -> None:
|
|
309
|
+
"""REPORT.md, SUMMARY.json and PROPOSALS.json next to the run files."""
|
|
310
|
+
(out_dir / "REPORT.md").write_text(render_report(results, meta), encoding="utf-8")
|
|
311
|
+
_write_json(out_dir / "SUMMARY.json", summary_payload(results, meta))
|
|
312
|
+
_write_json(out_dir / "PROPOSALS.json", [asdict(r.proposal) for r in results])
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# --- CLI ----------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _parse_args(argv: Sequence[str] | None) -> argparse.Namespace:
|
|
319
|
+
parser = argparse.ArgumentParser(prog="arka-py -m core.decisions.replay_report")
|
|
320
|
+
which = parser.add_mutually_exclusive_group(required=True)
|
|
321
|
+
which.add_argument("--all", action="store_true", help="every registered site")
|
|
322
|
+
which.add_argument("--sites", help="comma-separated site names")
|
|
323
|
+
parser.add_argument("--session", required=True, help="quality-gate session id")
|
|
324
|
+
parser.add_argument("--runs", type=int, default=2)
|
|
325
|
+
parser.add_argument("--live-window", choices=sorted(LIVE_WINDOWS), default="7d")
|
|
326
|
+
return parser.parse_args(argv)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def selected_sites(ns: argparse.Namespace) -> list[str]:
|
|
330
|
+
"""Registry order for ``--all``; ValueError names an unknown site."""
|
|
331
|
+
if ns.all:
|
|
332
|
+
return list(SITES)
|
|
333
|
+
names = [n.strip() for n in str(ns.sites).split(",") if n.strip()]
|
|
334
|
+
unknown = [n for n in names if n not in SITES]
|
|
335
|
+
if unknown or not names:
|
|
336
|
+
raise ValueError(f"unknown site(s): {', '.join(unknown) or '(none given)'}")
|
|
337
|
+
return names
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def output_dir(session: str) -> Path:
|
|
341
|
+
"""``~/.arkaos/quality-gate/<session>/replay-final`` (HOME read at call time)."""
|
|
342
|
+
if not SESSION_RE.fullmatch(session) or ".." in session:
|
|
343
|
+
raise ValueError(f"invalid session id: {session!r}")
|
|
344
|
+
return Path.home() / ".arkaos" / "quality-gate" / session / OUT_DIRNAME
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _validate(ns: argparse.Namespace) -> tuple[list[str], dict[str, list[ReplayCase]], Path]:
|
|
348
|
+
if ns.runs < 1:
|
|
349
|
+
raise ValueError("--runs must be >= 1")
|
|
350
|
+
names = selected_sites(ns)
|
|
351
|
+
out_dir = output_dir(ns.session)
|
|
352
|
+
# Every corpus is loaded before the first call: a bad one costs nothing.
|
|
353
|
+
return names, {n: load_corpus(n) for n in names}, out_dir
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _context(ns: argparse.Namespace, out_dir: Path) -> RunContext | None:
|
|
357
|
+
operator_cfg = load_decisions_config()
|
|
358
|
+
transport = resolve_transport(operator_cfg)
|
|
359
|
+
if transport is None:
|
|
360
|
+
return None
|
|
361
|
+
period = LIVE_WINDOWS[ns.live_window]
|
|
362
|
+
return RunContext(transport, no_cache_config(operator_cfg), operator_cfg, ns.runs,
|
|
363
|
+
out_dir, summarise(period).by_site, period)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _meta(ns: argparse.Namespace, results: Sequence[SiteResult]) -> dict[str, Any]:
|
|
367
|
+
measured = all(r.gate != "not-measured" for res in results for r in res.runs)
|
|
368
|
+
return {"session": ns.session, "generated_at": _now_iso(), "runs": ns.runs,
|
|
369
|
+
"live_window": ns.live_window, "all_measured": measured,
|
|
370
|
+
"cost_usd": round(sum(r.cost_usd for r in results), 10),
|
|
371
|
+
"cost_category": REPLAY_COST_CATEGORY,
|
|
372
|
+
"rule": {"min_reached_share": MIN_REACHED_SHARE,
|
|
373
|
+
"max_unavailable_share": MAX_UNAVAILABLE_SHARE}}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _prepare(argv: Sequence[str] | None) -> tuple[RunContext, dict[str, list[ReplayCase]],
|
|
377
|
+
argparse.Namespace] | int:
|
|
378
|
+
try:
|
|
379
|
+
ns = _parse_args(argv)
|
|
380
|
+
except SystemExit as exc:
|
|
381
|
+
return 0 if exc.code == 0 else 2
|
|
382
|
+
if bypassed():
|
|
383
|
+
print("error: ARKA_BYPASS_DECISIONS=1 — no replay calls", file=sys.stderr)
|
|
384
|
+
return 2
|
|
385
|
+
try:
|
|
386
|
+
names, corpora, out_dir = _validate(ns)
|
|
387
|
+
except (OSError, ValueError) as exc:
|
|
388
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
389
|
+
return 2
|
|
390
|
+
ctx = _context(ns, out_dir)
|
|
391
|
+
if ctx is None:
|
|
392
|
+
print("error: no transport (set OPENROUTER_API_KEY)", file=sys.stderr)
|
|
393
|
+
return 2
|
|
394
|
+
return ctx, {n: corpora[n] for n in names}, ns
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
398
|
+
"""CLI: 0 every run measured, 1 some run not measured, 2 usage / no transport."""
|
|
399
|
+
prepared = _prepare(argv)
|
|
400
|
+
if isinstance(prepared, int):
|
|
401
|
+
return prepared
|
|
402
|
+
ctx, corpora, ns = prepared
|
|
403
|
+
ctx.out_dir.mkdir(parents=True, exist_ok=True)
|
|
404
|
+
results = [run_site(site, cases, ctx) for site, cases in corpora.items()]
|
|
405
|
+
meta = _meta(ns, results)
|
|
406
|
+
write_outputs(results, meta, ctx.out_dir)
|
|
407
|
+
print(render_report(results, meta))
|
|
408
|
+
print(f"written: {ctx.out_dir}", file=sys.stderr)
|
|
409
|
+
return 0 if meta["all_measured"] else 1
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
if __name__ == "__main__":
|
|
413
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Shadow mode: Jev is asked off the hot path, the heuristic acts.
|
|
2
|
+
|
|
3
|
+
:func:`spawn_shadow` spools the call (0600, capped at
|
|
4
|
+
:data:`SPOOL_MAX_PENDING`) and detaches a worker — same pattern as
|
|
5
|
+
``core/hooks/stop.py`` (``Popen(start_new_session=True)``), so the hook
|
|
6
|
+
pays zero network latency. The worker (:func:`main`) asks Jev with
|
|
7
|
+
a 5 s ceiling, records telemetry with ``agree``, deletes its spool in
|
|
8
|
+
``finally`` and sweeps spools older than an hour.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import contextlib
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
import uuid
|
|
20
|
+
from collections.abc import Sequence
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from core.decisions.backoff import ensure_private_dir
|
|
24
|
+
from core.decisions.models import State
|
|
25
|
+
from core.decisions.paths import repo_root, spool_dir
|
|
26
|
+
from core.decisions.site import SiteCall
|
|
27
|
+
|
|
28
|
+
SPOOL_MAX_PENDING = 8
|
|
29
|
+
STALE_SPOOL_S = 3600.0
|
|
30
|
+
SHADOW_TIMEOUT_S = 5.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def spawn_shadow(calls: Sequence[SiteCall], state: State, session_id: str) -> bool:
|
|
34
|
+
"""Spool and detach; False when the spool is full or anything fails."""
|
|
35
|
+
if not calls:
|
|
36
|
+
return False
|
|
37
|
+
try:
|
|
38
|
+
spool = spool_dir()
|
|
39
|
+
# The spool holds the state in clear for seconds: every directory
|
|
40
|
+
# created on the way is 0700, the spool itself always (QG r1 m3).
|
|
41
|
+
ensure_private_dir(spool)
|
|
42
|
+
os.chmod(spool, 0o700)
|
|
43
|
+
# Workers sweep on exit, but a killed worker leaves its spool
|
|
44
|
+
# behind: without this, 8 orphans disable shadow mode for good.
|
|
45
|
+
sweep_stale()
|
|
46
|
+
if len(list(spool.glob("*.json"))) >= SPOOL_MAX_PENDING:
|
|
47
|
+
return False
|
|
48
|
+
path = _write_spool(spool, calls, state, session_id)
|
|
49
|
+
except (OSError, TypeError, ValueError):
|
|
50
|
+
return False
|
|
51
|
+
return _launch(path)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _write_spool(spool: Path, calls: Sequence[SiteCall], state: State, session_id: str) -> Path:
|
|
55
|
+
payload = {
|
|
56
|
+
"session_id": session_id,
|
|
57
|
+
"state": state,
|
|
58
|
+
"sites": [{"name": c.site.name, "heuristic": c.heuristic} for c in calls],
|
|
59
|
+
"created": time.time(),
|
|
60
|
+
}
|
|
61
|
+
data = json.dumps(payload, ensure_ascii=False, default=str)
|
|
62
|
+
path = spool / f"{uuid.uuid4().hex}.json"
|
|
63
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
64
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
65
|
+
fh.write(data)
|
|
66
|
+
return path
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _launch(path: Path) -> bool:
|
|
70
|
+
try:
|
|
71
|
+
subprocess.Popen(
|
|
72
|
+
[sys.executable, "-m", "core.decisions.shadow", str(path)],
|
|
73
|
+
stdin=subprocess.DEVNULL,
|
|
74
|
+
stdout=subprocess.DEVNULL,
|
|
75
|
+
stderr=subprocess.DEVNULL,
|
|
76
|
+
env={**os.environ, "PYTHONPATH": str(repo_root())},
|
|
77
|
+
start_new_session=True,
|
|
78
|
+
)
|
|
79
|
+
except (OSError, ValueError):
|
|
80
|
+
with contextlib.suppress(OSError):
|
|
81
|
+
path.unlink()
|
|
82
|
+
return False
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _in_spool(path: Path) -> bool:
|
|
87
|
+
try:
|
|
88
|
+
return path.resolve().parent == spool_dir().resolve()
|
|
89
|
+
except OSError:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _run_spool(path: Path) -> None:
|
|
94
|
+
from core.decisions.config import load_decisions_config
|
|
95
|
+
from core.decisions.engine import run_and_record
|
|
96
|
+
from core.decisions.registry import SITES
|
|
97
|
+
from core.decisions.transport import configured_model, resolve_transport
|
|
98
|
+
|
|
99
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
100
|
+
calls = [
|
|
101
|
+
SiteCall(SITES[s["name"]], s.get("heuristic"))
|
|
102
|
+
for s in payload.get("sites", []) if s.get("name") in SITES
|
|
103
|
+
]
|
|
104
|
+
cfg = load_decisions_config()
|
|
105
|
+
transport = resolve_transport(cfg, model=configured_model())
|
|
106
|
+
if not calls or transport is None:
|
|
107
|
+
return
|
|
108
|
+
run_and_record(
|
|
109
|
+
calls, payload.get("state", ""), modes={c.site.name: "shadow" for c in calls},
|
|
110
|
+
cfg=cfg, transport=transport, session_id=str(payload.get("session_id", "")),
|
|
111
|
+
timeout_s=SHADOW_TIMEOUT_S,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def sweep_stale(now: float | None = None) -> int:
|
|
116
|
+
"""Delete spools older than :data:`STALE_SPOOL_S`; count removed."""
|
|
117
|
+
ref = time.time() if now is None else now
|
|
118
|
+
removed = 0
|
|
119
|
+
for spool in spool_dir().glob("*.json"):
|
|
120
|
+
with contextlib.suppress(OSError):
|
|
121
|
+
if ref - spool.stat().st_mtime > STALE_SPOOL_S:
|
|
122
|
+
spool.unlink()
|
|
123
|
+
removed += 1
|
|
124
|
+
return removed
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main(argv: list[str] | None = None) -> int:
|
|
128
|
+
"""Worker entry: ``python -m core.decisions.shadow <spool.json>``."""
|
|
129
|
+
args = argv if argv is not None else sys.argv[1:]
|
|
130
|
+
if len(args) != 1:
|
|
131
|
+
return 2
|
|
132
|
+
path = Path(args[0])
|
|
133
|
+
if not _in_spool(path):
|
|
134
|
+
return 2
|
|
135
|
+
try:
|
|
136
|
+
_run_spool(path)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass # detached worker: nothing to report to
|
|
139
|
+
finally:
|
|
140
|
+
with contextlib.suppress(OSError):
|
|
141
|
+
path.unlink()
|
|
142
|
+
sweep_stale()
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
raise SystemExit(main())
|