From 8c833a1297df13cb92816b1b63d6fa65c39c50e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 05:30:34 +0000 Subject: [PATCH 1/3] docs: add eval architecture diagram to README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a second generated SVG to the Evals section covering how the four product workflows in openbot/domain/workflows.py (triage / review / fix / chat) are measured, in two lanes over one LangSmith substrate: OFFLINE (v0.1, built) benchmark datasets -> Inspect AI task -> thin solver -> openbot.evaluation -> the production agent and sandbox -> scorers + prediction export -> official harness grading -> writeback ONLINE (v0.3, planned) production runs -> auto-instrumented traces -> GitHub outcome signals -> online eval and annotation queue -> 30-day trend metrics Solid node borders are built today; dashed borders are specified in the eval PRD but not implemented, so the diagram shows the gap rather than hiding it — auto-tracing already exists, while the outcome-signal collector and the online judge do not, and triage still has no solver in evals/. A coverage strip names each workflow's offline and online suite. Also extracts the shared Style 2 SVG emitter into scripts/_diagram_svg.py so the two diagram generators cannot drift apart visually and the second one does not duplicate the first. The runtime diagram regenerates byte-identically across that refactor, so docs/assets/openbot-architecture.svg is unchanged in this commit. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MaszS22MH8osYjDjqgD5gr --- README.md | 11 + docs/assets/openbot-eval-architecture.svg | 175 +++++++++++ scripts/_diagram_svg.py | 227 +++++++++++++++ scripts/build_architecture_diagram.py | 182 +++--------- scripts/build_eval_architecture_diagram.py | 322 +++++++++++++++++++++ 5 files changed, 769 insertions(+), 148 deletions(-) create mode 100644 docs/assets/openbot-eval-architecture.svg create mode 100644 scripts/_diagram_svg.py create mode 100644 scripts/build_eval_architecture_diagram.py diff --git a/README.md b/README.md index affde00..3c0e832 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,17 @@ point yet. The alpha readiness criteria are documented in OpenBot uses evals as product verification, not as a separate demo agent. +![OpenBot eval architecture: an offline lane runs benchmark datasets through Inspect AI tasks, thin solvers and the openbot.evaluation facade into the production agent and sandbox, then scores and exports predictions; a planned online lane harvests traces and GitHub outcome signals from production runs into online eval and 30-day trend metrics. Both lanes write to LangSmith. A coverage strip lists the four product workflows: triage, review, fix and chat.](./docs/assets/openbot-eval-architecture.svg) + +Two lanes over one LangSmith substrate. The **offline** lane is what exists +today: a benchmark dataset is driven through the *production* agent and +sandbox via `openbot.evaluation`, so a suite measures the shipped code path +rather than an eval-only copy of it. The **online** lane is v0.3 in the +[eval PRD](./docs/prd/openbot-eval-prd.md) and is mostly unbuilt — the traces +are already there, the outcome-signal collector and the online judge are not. +Solid borders in the diagram are built; dashed borders are specified but not +implemented. + The target architecture is: ```text diff --git a/docs/assets/openbot-eval-architecture.svg b/docs/assets/openbot-eval-architecture.svg new file mode 100644 index 0000000..d42b192 --- /dev/null +++ b/docs/assets/openbot-eval-architecture.svg @@ -0,0 +1,175 @@ + + OpenBot eval architecture + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OpenBot — eval architecture + two lanes over one LangSmith substrate: offline benchmarks driven through the production agent, online signal from production runs + v0.1 built · v0.3 planned + + OFFLINE · v0.1 · BUILT + + ONLINE · v0.3 · PLANNED + + COVERAGE — THE FOUR PRODUCT WORKFLOWS + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + benchmark datasets + LangSmith mirrors published by make -C evals data-* + Martian CRB · SWE-bench V · SWE-QA-Pro · SWT-Bench + + 2 + Inspect AI task — one per benchmark + evals/tasks/: review_martian · fix_swe_bench · + chat_swe_qa · test_swt_bench + dataset + solver + scorers wired per task + + 3 + thin solver → openbot.evaluation + evals/solvers/*.py call runner.run_*_sample and adapt + the domain result into scorer / prediction shape + forbidden here: eval-only agents, sandboxes, tools + + 4 + production agent + real sandbox + the facade opens the prod sandbox, clones at base_sha, + then calls DeepAgents review / fix / chat / repro — + the same code the worker runs in production + + 5 + scorers + prediction export + review: LLM judge + overlap · chat: SWE-QA-Pro judge + fix / test: JSONL → official harness (Modal / docker) + → grade writeback as pass@1 feedback + + 1 + production workflow runs + triage · review · fix · chat on real repositories + live traffic, no benchmark dataset + + 2 + auto-instrumented traces + LANGSMITH_TRACING on the web and worker processes; + every agent run already lands in LangSmith today + this half of the online lane exists now + + 3 + outcome signals from GitHub + was the review comment resolved or acted on; did the + chat answer need a follow-up; was a triage label fixed + collector not built yet + + 4 + online eval + annotation queue + sampled LLM judge over live runs, plus human labels + from the LangSmith annotation queue + suites: triage_online · review_online · chat_online + + 5 + 30-day trend metrics + action_rate · resolved_rate · correct_30d · + macro_f1_30d · followup_rate · pass@1_90d + trend observation only — never blocks a merge + LangSmith — one substrate for both lanes + datasets · experiment rows · per-sample feedback · traces · annotation queue + every row carries dataset_version · solver_family=openbot_agent · capability · openbot_git_sha · mode + triage + off · triage_gitbugs (v0.2) + on · macro_f1_30d (v0.3) + no solver in evals/ yet + review + off · review_martian — mean F1 + on · action / resolved rate + 10% F1 drop blocks merge + fix + off · fix_swe_bench — pass@1 + on · fix_swe_bench_live 90d + rolling benchmark, not prod + chat + off · chat_swe_qa — 5-dim + on · correct_30d · followup + judge must exist; warn only + sample + solve() + run_*_sample + domain result + experiment row + traced + run id + sampled + scored + trend rows + + + offline path — built + + online path — planned + + writes to LangSmith + solid = built today · dashed = specified in the eval PRD but not built · test_swt_bench is a fifth surface, not a workflow + + diff --git a/scripts/_diagram_svg.py b/scripts/_diagram_svg.py new file mode 100644 index 0000000..1f1b782 --- /dev/null +++ b/scripts/_diagram_svg.py @@ -0,0 +1,227 @@ +"""Shared Style 2 ("Dark Terminal") SVG emitter for the diagrams under ``scripts/``. + +``build_architecture_diagram.py`` and ``build_eval_architecture_diagram.py`` +draw with the same palette, arrow markers and semantic attributes; only their +node/edge tables differ. Keeping the emitter in one place stops the two +diagrams drifting apart visually, and means both carry the same +``data-graph-role`` / ``data-node-id`` / ``data-edge-id`` contract that the +fireworks-tech-graph geometry and composition validators read. + +Not a general-purpose SVG library — it emits exactly the element shapes those +two diagrams need, in the paint order the validators expect (background → +containers → edges → node shapes → node text → edge labels → legend). +""" + +from __future__ import annotations + +import html + +# ── palette ──────────────────────────────────────────────────────────────── +BG_A, BG_B = "#0f0f1a", "#1a1a2e" +TXT, SUB, MUTE, LINE = "#e2e8f0", "#94a3b8", "#64748b", "#334155" + +# Semantic arrow colours. Each needs a marker pair emitted into . +FLOW, STATE, LLM, POLICY, EVAL = "#3b82f6", "#10b981", "#a855f7", "#f97316", "#64748b" +MARKER_OF = {FLOW: "m-flow", STATE: "m-state", LLM: "m-llm", POLICY: "m-policy", EVAL: "m-eval"} + +# (fill, stroke) per semantic node role. +THEMES = { + "external": ("#132a47", "#3b82f6"), + "compute": ("#1c1917", "#ea580c"), + "queue": ("#052e16", "#059669"), + "agent": ("#1e1b4b", "#7c3aed"), + "store": ("#052e16", "#059669"), + "sandbox": ("#292214", "#eab308"), + "config": ("#0f172a", "#334155"), + "eval": ("#0f172a", "#475569"), + "planned": ("#131a28", "#64748b"), +} + + +def esc(s: str) -> str: + return html.escape(s, quote=True) + + +class Canvas: + """Accumulates SVG source lines in validator-friendly paint order.""" + + def __init__( + self, + width: int, + height: int, + *, + title: str, + aria_label: str, + quality_profile: str = "showcase", + ) -> None: + self.width = width + self.height = height + self.lines: list[str] = [] + self._open(title, aria_label, quality_profile) + + # ── low-level ────────────────────────────────────────────────────────── + def add(self, line: str) -> None: + self.lines.append(line) + + def render(self) -> str: + return "\n".join(self.lines) + "\n" + + # ── document scaffold ────────────────────────────────────────────────── + def _open(self, title: str, aria_label: str, quality_profile: str) -> None: + add = self.add + add( + f'') + add(f" {esc(title)}") + add(" ") + add(" ") + add(' ') + add(f' ') + add(f' ') + add(" ") + for colour, mid in MARKER_OF.items(): + add( + f' ' + ) + add(f' ') + add(" ") + add( + f' ' + ) + add(f' ') + add(" ") + add(" ") + add( + f' ' + ) + + def title_block(self, heading: str, subtitle: str, tag: str) -> None: + add = self.add + add( + f' {esc(heading)}' + ) + add(f' {esc(subtitle)}') + add( + f' {esc(tag)}' + ) + + # ── graph elements ───────────────────────────────────────────────────── + def container( + self, cid: str, x: int, y: int, w: int, h: int, label: str, lx: int, ly: int + ) -> None: + self.add( + f' ' + ) + self.add( + f' {esc(label)}' + ) + + def edge( + self, + eid: str, + source: str, + target: str, + d: str, + colour: str, + *, + dash: str | None = None, + twoway: bool = False, + ) -> None: + mid = MARKER_OF[colour] + attrs = [ + 'data-graph-role="edge"', + f'data-edge-id="{eid}"', + f'data-source="{source}"', + f'data-target="{target}"', + f'd="{d}"', + 'fill="none"', + f'stroke="{colour}"', + 'stroke-width="1.8"', + f'marker-end="url(#{mid})"', + ] + if twoway: + attrs.append(f'marker-start="url(#{mid}-start)"') + if dash: + attrs.append(f'stroke-dasharray="{dash}"') + self.add(f" ") + + def node( + self, nid: str, x: int, y: int, w: int, h: int, theme: str, *, dashed: bool = False + ) -> None: + fill, stroke = THEMES[theme] + dash_attr = ' stroke-dasharray="6,4"' if dashed else "" + self.add( + f' ' + ) + + def badge(self, cx: int, cy: int, text: str, stroke: str) -> None: + self.add( + f' ' + ) + self.add( + f' {esc(text)}' + ) + + def text( + self, x: int, y: int, size: int, weight: int, fill: str, txt: str, *, anchor: str = "start" + ) -> None: + anchor_attr = f' text-anchor="{anchor}"' if anchor != "start" else "" + self.add( + f' {esc(txt)}' + ) + + def edge_label( + self, eid: str, x: int, y: int, size: int, colour: str, txt: str, *, anchor: str = "start" + ) -> None: + anchor_attr = f' text-anchor="{anchor}"' if anchor != "start" else "" + self.add( + f' {esc(txt)}' + ) + + def legend( + self, + items: list[tuple[str, str | None, bool, str]], + *, + y: int, + footer: str, + footer_y: int, + ) -> None: + """Emit the swatch row plus a footer note, both excluded from geometry.""" + self.add(' ') + lx = 36.0 + for colour, dash, twoway, text in items: + mid = MARKER_OF[colour] + extra = f' stroke-dasharray="{dash}"' if dash else "" + start = f' marker-start="url(#{mid}-start)"' if twoway else "" + self.add( + f' ' + ) + self.add( + f' {esc(text)}' + ) + lx += 22 + 30 + len(text) * 5.9 + self.add( + f' {esc(footer)}' + ) + self.add(" ") diff --git a/scripts/build_architecture_diagram.py b/scripts/build_architecture_diagram.py index e17d79d..6c1667f 100644 --- a/scripts/build_architecture_diagram.py +++ b/scripts/build_architecture_diagram.py @@ -17,27 +17,11 @@ from __future__ import annotations -import html from pathlib import Path -W, H = 1000, 960 - -BG_A, BG_B = "#0f0f1a", "#1a1a2e" -TXT, SUB, MUTE, LINE = "#e2e8f0", "#94a3b8", "#64748b", "#334155" - -THEMES = { - "external": ("#132a47", "#3b82f6"), - "compute": ("#1c1917", "#ea580c"), - "queue": ("#052e16", "#059669"), - "agent": ("#1e1b4b", "#7c3aed"), - "store": ("#052e16", "#059669"), - "sandbox": ("#292214", "#eab308"), - "config": ("#0f172a", "#334155"), - "eval": ("#0f172a", "#475569"), -} +from _diagram_svg import EVAL, FLOW, LLM, MUTE, POLICY, STATE, SUB, THEMES, TXT, Canvas -FLOW, STATE, LLM, POLICY, EVAL = "#3b82f6", "#10b981", "#a855f7", "#f97316", "#64748b" -MARKER_OF = {FLOW: "m-flow", STATE: "m-state", LLM: "m-llm", POLICY: "m-policy", EVAL: "m-eval"} +W, H = 1000, 960 # --------------------------------------------------------------- geometry grid SPINE_X, SPINE_W = 296, 410 # centre column, centre line x = 501 @@ -320,154 +304,56 @@ (EVAL, "4,3", False, "eval reuse (offline)"), ] -lines: list[str] = [] -A = lines.append - - -def esc(s: str) -> str: - return html.escape(s, quote=True) - - -# --------------------------------------------------------------- header + defs -A(f'') -A(" OpenBot runtime architecture") -A(" ") -A(" ") -A(' ') -A(f' ') -A(f' ') -A(" ") -for colour, mid in MARKER_OF.items(): - A(f' ') - A(f' ') - A(" ") - A( - f' ' - ) - A(f' ') - A(" ") -A(" ") -# --------------------------------------------------------------- 1. background -A(f' ') - -# --------------------------------------------------------------- title block -A( - f' ' - "OpenBot — runtime architecture" -) -A( - f' self-hosted GitHub App: webhook →' - " preflight gates → queued worker → sandboxed agent → write-back" +canvas.title_block( + "OpenBot — runtime architecture", + "self-hosted GitHub App: webhook → preflight gates → queued worker → sandboxed agent" + " → write-back", + "v0.1 · pre-alpha", ) -A(f' v0.1 · pre-alpha') -# --------------------------------------------------------------- 2. containers for cid, cx, cy, cw, ch, label, lx, ly in CONTAINERS: - A( - f' ' - ) - A( - f' {esc(label)}' - ) + canvas.container(cid, cx, cy, cw, ch, label, lx, ly) -# --------------------------------------------------------------- 3. edges for eid, src, tgt, d, colour, dash, twoway, *_ in EDGES: - mid = MARKER_OF[colour] - attrs = [ - 'data-graph-role="edge"', - f'data-edge-id="{eid}"', - f'data-source="{src}"', - f'data-target="{tgt}"', - f'd="{d}"', - 'fill="none"', - f'stroke="{colour}"', - 'stroke-width="1.8"', - f'marker-end="url(#{mid})"', - ] - if twoway: - attrs.append(f'marker-start="url(#{mid}-start)"') - if dash: - attrs.append(f'stroke-dasharray="{dash}"') - A(f" ") + canvas.edge(eid, src, tgt, d, colour, dash=dash, twoway=twoway) -# --------------------------------------------------------------- 4. node shapes for nid, (x, y, w, h, theme) in NODES.items(): - fill, stroke = THEMES[theme] - dash = ' stroke-dasharray="6,4"' if theme == "eval" else "" - A( - f' ' - ) + canvas.node(nid, x, y, w, h, theme, dashed=theme == "eval") -# --------------------------------------------------------------- 5. node text for nid, (badge, text_x, rows) in BODY.items(): x, y, w, h, theme = NODES[nid] _, stroke = THEMES[theme] if badge: - cy = y + rows[0][0] - 5 - A( - f' ' - ) - A( - f' {badge}' - ) + canvas.badge(x + 20, y + rows[0][0] - 5, badge, stroke) for dy, size, weight, fill_c, txt in rows: if text_x is None: - A( - f' {esc(txt)}' - ) + canvas.text(x + w // 2, y + dy, size, weight, fill_c, txt, anchor="middle") else: - A( - f' {esc(txt)}' - ) + canvas.text(text_x, y + dy, size, weight, fill_c, txt) -# --------------------------------------------------------------- 6. edge labels for eid, _src, _tgt, _d, colour, _dash, _twoway, label, lx, ly, anchor, size in EDGES: - anchor_attr = f' text-anchor="{anchor}"' if anchor != "start" else "" - A( - f' {esc(label)}' - ) - -# --------------------------------------------------------------- 7. legend -A(' ') -lx = 36.0 -for colour, dash, twoway, text in LEGEND: - mid = MARKER_OF[colour] - extra = f' stroke-dasharray="{dash}"' if dash else "" - start = f' marker-start="url(#{mid}-start)"' if twoway else "" - A( - f' ' - ) - A(f' {esc(text)}') - lx += 22 + 30 + len(text) * 5.9 -A( - f' cross-cutting — Sentry · LangSmith ·' - " Langfuse · Prometheus tracing on both processes; audit_log is the product-facing ledger" + canvas.edge_label(eid, lx, ly, size, colour, label, anchor=anchor) + +canvas.legend( + LEGEND, + y=916, + footer="cross-cutting — Sentry · LangSmith · Langfuse · Prometheus tracing on both" + " processes; audit_log is the product-facing ledger", + footer_y=942, ) -A(" ") -A("") +canvas.add("") OUT = Path(__file__).resolve().parent.parent / "docs" / "assets" / "openbot-architecture.svg" @@ -480,8 +366,8 @@ def main() -> int: produce an untracked copy the README cannot see. """ OUT.parent.mkdir(parents=True, exist_ok=True) - OUT.write_text("\n".join(lines) + "\n", encoding="utf-8") - print(f"wrote {OUT} ({len(lines)} lines)") + OUT.write_text(canvas.render(), encoding="utf-8") + print(f"wrote {OUT} ({len(canvas.lines)} lines)") return 0 diff --git a/scripts/build_eval_architecture_diagram.py b/scripts/build_eval_architecture_diagram.py new file mode 100644 index 0000000..5335a3e --- /dev/null +++ b/scripts/build_eval_architecture_diagram.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 +"""Regenerate ``docs/assets/openbot-eval-architecture.svg`` (embedded in README.md). + +Shows the two eval lanes described by ``docs/prd/openbot-eval-prd.md`` and +``docs/prd/openbot-eval-suites.md``: + + * OFFLINE (v0.1, built) — benchmark datasets driven through the *production* + agent path via ``openbot.evaluation``, scored, then exported to LangSmith. + * ONLINE (v0.3, planned) — signal harvested from real production runs. + +plus a coverage strip for the four product workflows in +``openbot/domain/workflows.py`` (triage / review / fix / chat). + +Solid borders are built today; dashed borders are specified in the eval PRD +but not implemented. Edit the tables below and re-run: + + python3 scripts/build_eval_architecture_diagram.py + +Layout follows the fireworks-tech-graph architecture-diagram rules (Style 2, +"Dark Terminal") under the ``showcase`` composition profile. The shared +emitter lives in ``scripts/_diagram_svg.py``. +""" + +from __future__ import annotations + +from pathlib import Path + +from _diagram_svg import FLOW, LLM, MUTE, STATE, SUB, THEMES, TXT, Canvas + +W, H = 1060, 1012 + +# ── grid ─────────────────────────────────────────────────────────────────── +LANE_W = 432 +A_X, B_X = 58, 570 # lane node left edges +A_CX, B_CX = A_X + LANE_W // 2, B_X + LANE_W // 2 +ROWS = [(132, 66), (238, 74), (352, 74), (466, 80), (586, 74)] +LS_Y, LS_H = 722, 66 +CARD_Y, CARD_H, CARD_W = 858, 70, 207 +CARD_X = [56, 303, 550, 797] + +# id: (x, y, w, h, theme, dashed) +NODES: dict[str, tuple[int, int, int, int, str, bool]] = {} +for _i, (_y, _h) in enumerate(ROWS, start=1): + NODES[f"a{_i}"] = (A_X, _y, LANE_W, _h, "", False) + NODES[f"b{_i}"] = (B_X, _y, LANE_W, _h, "", False) + +_A_THEMES = ["store", "external", "external", "agent", "compute"] +_B_THEMES = [ + ("external", False), + ("store", False), + ("planned", True), + ("agent", True), + ("sandbox", True), +] +for _i, _theme in enumerate(_A_THEMES, start=1): + _x, _y, _w, _h, _, _ = NODES[f"a{_i}"] + NODES[f"a{_i}"] = (_x, _y, _w, _h, _theme, False) +for _i, (_theme, _dashed) in enumerate(_B_THEMES, start=1): + _x, _y, _w, _h, _, _ = NODES[f"b{_i}"] + NODES[f"b{_i}"] = (_x, _y, _w, _h, _theme, _dashed) + +NODES["ls"] = (36, LS_Y, 988, LS_H, "store", False) +for _i, _cx in enumerate(CARD_X, start=1): + NODES[f"c{_i}"] = (_cx, CARD_Y, CARD_W, CARD_H, "eval" if _i == 1 else "external", _i == 1) + +# id, x, y, w, h, label, label x, label y +CONTAINERS = [ + ("cA", 36, 96, 476, 586, "OFFLINE · v0.1 · BUILT", 50, 116), + ("cB", 548, 96, 476, 586, "ONLINE · v0.3 · PLANNED", 562, 116), + ("cC", 36, 822, 988, 126, "COVERAGE — THE FOUR PRODUCT WORKFLOWS", 50, 842), +] + +# id, source, target, d, colour, dash, label, label x, label y +EDGES = [ + ("ea1", "a1", "a2", f"M {A_CX},198 L {A_CX},238", FLOW, None, "sample", 286, 221), + ("ea2", "a2", "a3", f"M {A_CX},312 L {A_CX},352", FLOW, None, "solve()", 286, 335), + ("ea3", "a3", "a4", f"M {A_CX},426 L {A_CX},466", FLOW, None, "run_*_sample", 286, 449), + ("ea4", "a4", "a5", f"M {A_CX},546 L {A_CX},586", FLOW, None, "domain result", 286, 569), + ("ea5", "a5", "ls", f"M {A_CX},660 L {A_CX},{LS_Y}", STATE, None, "experiment row", 286, 697), + ("eb1", "b1", "b2", f"M {B_CX},198 L {B_CX},238", LLM, "5,4", "traced", 798, 221), + ("eb2", "b2", "b3", f"M {B_CX},312 L {B_CX},352", LLM, "5,4", "run id", 798, 335), + ("eb3", "b3", "b4", f"M {B_CX},426 L {B_CX},466", LLM, "5,4", "sampled", 798, 449), + ("eb4", "b4", "b5", f"M {B_CX},546 L {B_CX},586", LLM, "5,4", "scored", 798, 569), + ("eb5", "b5", "ls", f"M {B_CX},660 L {B_CX},{LS_Y}", STATE, None, "trend rows", 798, 697), +] + +# node id -> (badge, text x or None for centred, [(dy, size, weight, fill, text)]) +BODY = { + "a1": ( + "1", + A_X + 40, + [ + (22, 12, 700, TXT, "benchmark datasets"), + (40, 10, 400, SUB, "LangSmith mirrors published by make -C evals data-*"), + (56, 10, 400, SUB, "Martian CRB · SWE-bench V · SWE-QA-Pro · SWT-Bench"), + ], + ), + "a2": ( + "2", + A_X + 40, + [ + (22, 12, 700, TXT, "Inspect AI task — one per benchmark"), + (40, 10, 400, SUB, "evals/tasks/: review_martian · fix_swe_bench ·"), + (55, 10, 400, SUB, "chat_swe_qa · test_swt_bench"), + (70, 10, 400, MUTE, "dataset + solver + scorers wired per task"), + ], + ), + "a3": ( + "3", + A_X + 40, + [ + (22, 12, 700, TXT, "thin solver → openbot.evaluation"), + (40, 10, 400, SUB, "evals/solvers/*.py call runner.run_*_sample and adapt"), + (55, 10, 400, SUB, "the domain result into scorer / prediction shape"), + (70, 10, 400, MUTE, "forbidden here: eval-only agents, sandboxes, tools"), + ], + ), + "a4": ( + "4", + A_X + 40, + [ + (22, 12, 700, TXT, "production agent + real sandbox"), + (40, 10, 400, SUB, "the facade opens the prod sandbox, clones at base_sha,"), + (55, 10, 400, SUB, "then calls DeepAgents review / fix / chat / repro —"), + (70, 10, 400, SUB, "the same code the worker runs in production"), + ], + ), + "a5": ( + "5", + A_X + 40, + [ + (22, 12, 700, TXT, "scorers + prediction export"), + (40, 10, 400, SUB, "review: LLM judge + overlap · chat: SWE-QA-Pro judge"), + (55, 10, 400, SUB, "fix / test: JSONL → official harness (Modal / docker)"), + (70, 10, 400, MUTE, "→ grade writeback as pass@1 feedback"), + ], + ), + "b1": ( + "1", + B_X + 40, + [ + (22, 12, 700, TXT, "production workflow runs"), + (40, 10, 400, SUB, "triage · review · fix · chat on real repositories"), + (56, 10, 400, MUTE, "live traffic, no benchmark dataset"), + ], + ), + "b2": ( + "2", + B_X + 40, + [ + (22, 12, 700, TXT, "auto-instrumented traces"), + (40, 10, 400, SUB, "LANGSMITH_TRACING on the web and worker processes;"), + (55, 10, 400, SUB, "every agent run already lands in LangSmith today"), + (70, 10, 400, MUTE, "this half of the online lane exists now"), + ], + ), + "b3": ( + "3", + B_X + 40, + [ + (22, 12, 700, TXT, "outcome signals from GitHub"), + (40, 10, 400, SUB, "was the review comment resolved or acted on; did the"), + (55, 10, 400, SUB, "chat answer need a follow-up; was a triage label fixed"), + (70, 10, 400, MUTE, "collector not built yet"), + ], + ), + "b4": ( + "4", + B_X + 40, + [ + (22, 12, 700, TXT, "online eval + annotation queue"), + (40, 10, 400, SUB, "sampled LLM judge over live runs, plus human labels"), + (55, 10, 400, SUB, "from the LangSmith annotation queue"), + (70, 10, 400, MUTE, "suites: triage_online · review_online · chat_online"), + ], + ), + "b5": ( + "5", + B_X + 40, + [ + (22, 12, 700, TXT, "30-day trend metrics"), + (40, 10, 400, SUB, "action_rate · resolved_rate · correct_30d ·"), + (55, 10, 400, SUB, "macro_f1_30d · followup_rate · pass@1_90d"), + (70, 10, 400, MUTE, "trend observation only — never blocks a merge"), + ], + ), + "ls": ( + None, + None, + [ + (24, 13, 700, TXT, "LangSmith — one substrate for both lanes"), + ( + 43, + 11, + 400, + SUB, + "datasets · experiment rows · per-sample feedback · traces · annotation queue", + ), + ( + 59, + 10, + 400, + MUTE, + "every row carries dataset_version · solver_family=openbot_agent · capability ·" + " openbot_git_sha · mode", + ), + ], + ), + "c1": ( + None, + CARD_X[0] + 14, + [ + (20, 12, 700, TXT, "triage"), + (36, 10, 400, SUB, "off · triage_gitbugs (v0.2)"), + (51, 10, 400, SUB, "on · macro_f1_30d (v0.3)"), + (66, 10, 400, MUTE, "no solver in evals/ yet"), + ], + ), + "c2": ( + None, + CARD_X[1] + 14, + [ + (20, 12, 700, TXT, "review"), + (36, 10, 400, SUB, "off · review_martian — mean F1"), + (51, 10, 400, SUB, "on · action / resolved rate"), + (66, 10, 400, MUTE, "10% F1 drop blocks merge"), + ], + ), + "c3": ( + None, + CARD_X[2] + 14, + [ + (20, 12, 700, TXT, "fix"), + (36, 10, 400, SUB, "off · fix_swe_bench — pass@1"), + (51, 10, 400, SUB, "on · fix_swe_bench_live 90d"), + (66, 10, 400, MUTE, "rolling benchmark, not prod"), + ], + ), + "c4": ( + None, + CARD_X[3] + 14, + [ + (20, 12, 700, TXT, "chat"), + (36, 10, 400, SUB, "off · chat_swe_qa — 5-dim"), + (51, 10, 400, SUB, "on · correct_30d · followup"), + (66, 10, 400, MUTE, "judge must exist; warn only"), + ], + ), +} + +LEGEND = [ + (FLOW, None, False, "offline path — built"), + (LLM, "5,4", False, "online path — planned"), + (STATE, None, False, "writes to LangSmith"), +] + +canvas = Canvas( + W, + H, + title="OpenBot eval architecture", + aria_label=( + "OpenBot eval architecture: an offline lane runs benchmark datasets through Inspect AI" + " tasks, thin solvers and the openbot.evaluation facade into the production agent and" + " sandbox, then scores and exports predictions; a planned online lane harvests traces and" + " GitHub outcome signals from production runs into online eval and 30-day trend metrics." + " Both lanes write to LangSmith. A coverage strip lists the four product workflows:" + " triage, review, fix and chat." + ), +) + +canvas.title_block( + "OpenBot — eval architecture", + "two lanes over one LangSmith substrate: offline benchmarks driven through the production" + " agent, online signal from production runs", + "v0.1 built · v0.3 planned", +) + +for cid, cx, cy, cw, ch, label, lx, ly in CONTAINERS: + canvas.container(cid, cx, cy, cw, ch, label, lx, ly) + +for eid, src, tgt, d, colour, dash, *_ in EDGES: + canvas.edge(eid, src, tgt, d, colour, dash=dash) + +for nid, (x, y, w, h, theme, dashed) in NODES.items(): + canvas.node(nid, x, y, w, h, theme, dashed=dashed) + +for nid, (badge, text_x, rows) in BODY.items(): + x, y, w, h, theme, _ = NODES[nid] + _, stroke = THEMES[theme] + if badge: + canvas.badge(x + 20, y + rows[0][0] - 5, badge, stroke) + for dy, size, weight, fill_c, txt in rows: + if text_x is None: + canvas.text(x + w // 2, y + dy, size, weight, fill_c, txt, anchor="middle") + else: + canvas.text(text_x, y + dy, size, weight, fill_c, txt) + +for eid, _src, _tgt, _d, colour, _dash, label, lx, ly in EDGES: + canvas.edge_label(eid, lx, ly, 10, colour, label) + +canvas.legend( + LEGEND, + y=974, + footer="solid = built today · dashed = specified in the eval PRD but not built ·" + " test_swt_bench is a fifth surface, not a workflow", + footer_y=998, +) +canvas.add("") + +OUT = Path(__file__).resolve().parent.parent / "docs" / "assets" / "openbot-eval-architecture.svg" + + +def main() -> int: + """Write the diagram to its one tracked location (see the sibling script).""" + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_text(canvas.render(), encoding="utf-8") + print(f"wrote {OUT} ({len(canvas.lines)} lines)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 38910bd2646cfe8d50ae578ff03502a030d47f99 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 09:02:59 +0000 Subject: [PATCH 2/3] fix: draw the built offline lane with a solid container border MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The eval diagram's footer states "solid = built today · dashed = specified in the eval PRD but not built", but Canvas.container hardcoded stroke-dasharray, so the BUILT offline lane and the coverage panel were both drawn in the not-built style — the diagram contradicted its own legend on the one distinction it exists to make. container() now takes dashed=True by default, so a dashed boundary stays the plain grouping convention and the runtime diagram is untouched; the eval diagram passes the status explicitly per container. The runtime SVG regenerates byte-identically. Reported by CodeRabbit on #134. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MaszS22MH8osYjDjqgD5gr --- docs/assets/openbot-eval-architecture.svg | 4 ++-- scripts/_diagram_svg.py | 22 ++++++++++++++++++++-- scripts/build_eval_architecture_diagram.py | 12 ++++++------ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/docs/assets/openbot-eval-architecture.svg b/docs/assets/openbot-eval-architecture.svg index d42b192..6a90eb2 100644 --- a/docs/assets/openbot-eval-architecture.svg +++ b/docs/assets/openbot-eval-architecture.svg @@ -45,11 +45,11 @@ OpenBot — eval architecture two lanes over one LangSmith substrate: offline benchmarks driven through the production agent, online signal from production runs v0.1 built · v0.3 planned - + OFFLINE · v0.1 · BUILT ONLINE · v0.3 · PLANNED - + COVERAGE — THE FOUR PRODUCT WORKFLOWS diff --git a/scripts/_diagram_svg.py b/scripts/_diagram_svg.py index 1f1b782..251445e 100644 --- a/scripts/_diagram_svg.py +++ b/scripts/_diagram_svg.py @@ -119,12 +119,30 @@ def title_block(self, heading: str, subtitle: str, tag: str) -> None: # ── graph elements ───────────────────────────────────────────────────── def container( - self, cid: str, x: int, y: int, w: int, h: int, label: str, lx: int, ly: int + self, + cid: str, + x: int, + y: int, + w: int, + h: int, + label: str, + lx: int, + ly: int, + *, + dashed: bool = True, ) -> None: + """Draw a grouping boundary. + + ``dashed`` defaults to True because a dashed boundary is the plain + grouping convention. Pass ``dashed=False`` in a diagram that also + uses dash to mean "not built yet", so a built group is not drawn in + the unbuilt style. + """ + dash_attr = ' stroke-dasharray="6,5"' if dashed else "" self.add( f' ' + f"{dash_attr}/>" ) self.add( f' Date: Thu, 20 Aug 2026 09:07:19 +0000 Subject: [PATCH 3/3] docs: add OpenBot logo and repo social preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a mark for the README header and the GitHub social preview. The subject is a tortoise: it carries its own shell the way a self-hosted deployment carries its own keys and data, and it is calm and unhurried, which matches a bot that comments and opens PRs but never auto-merges. Built to the ip-as-logo design brief — one dominant silhouette from five large shapes, one species-defining feature (the broad flat shell), two IP colors plus one solid background, and no outlines, gradients or texture, so it stays readable at 32x32. Assets: docs/assets/openbot-logo.svg 512x512 mark, README header docs/assets/openbot-logo.png raster for avatar uploads docs/assets/openbot-social-preview.svg 1280x640 source docs/assets/openbot-social-preview.png upload via Settings > Social preview Authored as vector rather than raster: no image generator is configured in this environment, and the brief's complexity budget — a handful of flat geometric shapes in three flat colors — is expressible exactly in SVG, which also scales and diffs better for a README asset. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MaszS22MH8osYjDjqgD5gr --- README.md | 18 +++++++++++++----- docs/assets/openbot-logo.png | Bin 0 -> 14541 bytes docs/assets/openbot-logo.svg | 11 +++++++++++ docs/assets/openbot-social-preview.png | Bin 0 -> 34881 bytes docs/assets/openbot-social-preview.svg | 20 ++++++++++++++++++++ 5 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 docs/assets/openbot-logo.png create mode 100644 docs/assets/openbot-logo.svg create mode 100644 docs/assets/openbot-social-preview.png create mode 100644 docs/assets/openbot-social-preview.svg diff --git a/README.md b/README.md index 3c0e832..738cce2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ -# OpenBot +

+ OpenBot +

-> Open-source, self-hosted GitHub maintenance automation for teams that want to own their bot, prompts, models, data, and cost controls. +

OpenBot

-[![status](https://img.shields.io/badge/status-pre--alpha-orange)](./docs/prd/openbot-prd.md) -[![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE) -[![python](https://img.shields.io/badge/python-3.12%2B-blue)](./pyproject.toml) +

+ Open-source, self-hosted GitHub maintenance automation for teams that want to own their bot, prompts, models, data, and cost controls. +

+ +

+ status + license + python +

OpenBot is a GitHub App backend for maintainer workflows: diff --git a/docs/assets/openbot-logo.png b/docs/assets/openbot-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..703b559b6fd29ca3a4d3c3cc41b54cf7cd0866d6 GIT binary patch literal 14541 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 JKupj3{{uA-5TpPA literal 0 HcmV?d00001 diff --git a/docs/assets/openbot-logo.svg b/docs/assets/openbot-logo.svg new file mode 100644 index 0000000..e6f1bce --- /dev/null +++ b/docs/assets/openbot-logo.svg @@ -0,0 +1,11 @@ + + OpenBot + + + + + + + + diff --git a/docs/assets/openbot-social-preview.png b/docs/assets/openbot-social-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..9c8ca4379aa5a35200bba0fcf2b28109f633dc6a GIT binary patch literal 34881 zcmeFY^;c9;`v*E8p&+3kDAFAgO6Q0ONDBx^44|}hcXu~P;{ehjUBiF~NDd7{jndsP zz`)EM-}igh{R{4I?_$gouU+0059Ey?d(#0N`VrxIy=E zu@}|kH<#E8!Dki4w*bt)zd}f9DgeL;Pdc&tob ztXED1zE4yZ#F=3xeR-(KurXi9GeCOZgVK%@qH?76824KkD{Ju_?^$P^rj}(YNzT3> zFq6MiNb)QP4qtBDLcA?V`hbIdF96`l(boQ-27nI_SKwdk^^^PE|LM_T1l;@A_z(>l(tGHa#%x^ zCVjq~=ckNmn+ZG!VT=MRIM$r$G-ppj(Ira`<&87C3)ShD*)E3mZOd9ri&fNB0sl6t zGI`K9Dg!bg#4~J+wgyQ$ekIzmPP|uJ)yDpck6svzcQw3M!tE7S*4H5P$#*m zzOebcxa37?^LbcP#qXVtf1bcNC$7L!QkgJ~zgZm4i4R7s$fRn8RPt%)!t;BN&sYA2%zy818NOVYq?iehtWZCw9a=O@hYsj!AMR!s z8i1Kt?f)G2$Pru|_-a-a7*o>uG}+b4q_^1|WIs{K;4==aD{Kyr36ng1ik^+w={;9N z1ie_a$XWhUU0qXC!_Cjj#mDtREZy_f*8D_K}w*Uo0i}TpC zFLN_bCwl~0NB>apig{(OR1CVPDS23|tn4mHKjCQBrM75mXDkcdegxk;9Zaq5s@JG$ zX})IQgll;@U3;30<)emYJ83x52wUd%0ZRhZZu$(?7RVYKd5Dv^@nsG0Cr^o3OsCI;y1TfHs^6xCj5mJ81yoy47MRGw^RLB zrSU&>GSe(;c_lk5ZhuR3#G^~BX(md~-1$dZ&EAJ~g-4P{Ed;60LB%@s5PFCH9)FLX z{32;20P8Is4_KDE1#GS;LM4AXxdYxmz!flfrg{2sZ#_q7Qq;MuShTG@dsfBl{6vL* zCsS(D*wEO;OUGt&^0a8rLK39?b@w&&AAe7#RZXzJFk6ovFKfLSoM=qXgs{{{L@^IGdaUx??91G8wWUkfpGIR@oSixRViP_0FM$&)je zHZe2jo>_fi`A87xkpcco({2iWodn|7Bk-@#OJdnHw}3&GyA(d&QDc+3jozV7>6)7Rd~7#l zzUzO7S%gU0?!k>ctgl-dfQEI}XA|}9{qJ^fAK%1vuD0(DoX1`|nGS=QoF^%eRk>3B zf{&h(rlg`7e?VNsK=*#S2uTo6>I9e0>P@NFaqCjnJ#MmDc zwyjyq5r-M!_h#KezM)_{`r75ZW+@-ZgATXl)g=9J~^?#C>3(sH)vMd zxi?xHP$_vw6k^IXWF+p{KU!4VbzSY=MnxEL5!1dMov5Z>#3GbCYT)sC5v)Obr}SlQN=xlwKMp`t5ugw|mGuV8So*+GDOWcWy*aSQ%1eI=4nKPFReY@VsI=|D zFICAsJsR$%je=gTj$(2o(BCwgsknD34SZcjJCF&GgQ;bUp{+-cF9sK3d(Jo$8_XW8L$PfUGs{MXc%s1%J`a#vLhcp0(o zc-^h5y}29W^eIXv)S zufWbV0vjHMiD$T*BGA$uo<~D3KCBp%0I*7qgLP&`xSK8-?0EWndw{>rS~0=*&lfH_HzSl%Yxe0kX8w%4tFJykzQK33 z^gljG@UzkgFTPNtb?Vz5j=bEPD$i?KC45w0ay7mw92;CqjRyx%zv^ME1gdHkt}=;%SD==l!x_f{UFAnej3|K0df{ zItCSy&J$pK+*>PoO6SE~%y8SknW@mY?`r9bruH;G9Z|5nfA0^0HBQ9YvwM$!5=iYn znT5}U7e1m1_56rH`L7aeRRw%&7nqvaD8#p+P)s=#k?Me}jF8 z3c4N2fzn5#*}Tb~O0r(fU3CEwapOt6IEF15qamYo~9j;;Aht-Ovcxo1*=DYgM7Nx++1xo8=MyYvMA)lCa`Jo4?1EeOEshC>| z#$YYKL%@-w^$>l>w)H%r^?pgmc8~X+8Wb7NDA*E{(qz}P2|%uAMJTP>$iWkW-0}4!`;ZoZ;Zl6X_AtJeaP=XFU-5;`S)bt$_G^xH_@@yqpXgVE+Xuzk_rH z_lvr(#xexDSha(V2dINMh4WDuQ+^&Uo7Hgs?35ZGpV`&xDU7htH?AFImHp1GcI5Ki zCN*@V4Kw+)D+8x{5Nj-H5|bLkPF(Lz)>@=&i72KC1%uex)USRDN-fVc72;d35s<~{ z<_7Z2!O%V?4Zae(_Sc_RNK4k9$%7(QZmuOo`E29}rBORMp65i?22USdd7IdoTp=q%Grgn-+#ql|NirQZP#Gj&8NWQhh8W3vv7%iJ|pu+`ENSQlo{U;18~$vpta zU*adD7RT|?(3`0wW6x-sh_n{(K&jy<2GQX^&kuGD{sf&2 zeP(p~?7Q>xCKo05dSYch4e`K?r$)|Ego8^LhnZ->`EQn{6DB8xvpzBU$pad{F zQrV0>^1mE&<47cPKZU3IMXEimx!MWVzAO{I!Q*gyCieGm3wpCt6nTTsB|!`K@Iy-$ zScnUpwQbKKe^7IjiU~mtT;HuoANn4SjXfkJ2-*b0xU=8b5+%@_q*%Qp)T0i`*eDR` ze@Y&BKBriR*bni;l~p`U8d z{DG1ln63B0X3FO#OhF#cTU^L6nbHjoYVo^Xj9sjFek-^p-vSJfOZizdSKq{G2KOWm zH0ZKH1@GSy#lqh^JLg)IpcFAn{7exd6 z&1!EOq32b->AV)=s9FC8$7RogfS1>8J4iAjTRU5D*IgK@)?ny$bXb+gCJW6`&E~!7F5e2dF)zXZA8B zz82w)ibM_Vkr_3EK?S#{S+Jf=u8dG?vVy|I^jSnSs4MTs%yo6h_E4L{RY_6i^8Bj3vH31)PDVc!AZ_#&9Zq^;CB0hh%!ql^N z+SND7s_cEPgTJt?KUjysmd;i4u)ah3GZSg=VV)e7G@WnJt#+$`%uyZ!fJ0%^9rL#fqMei4 zC|QlDlt~#ACqZ!WgFU3FQ{ti#=sqorj~93Tcg32DDi0U3_1z)vj!C>1&!nYuFN#!E%QH>V1z@zQ~V!x}J{=InlT^z&B&HHWpYx$Le=q$zW)eRtYLjX@<&c@}Nv%Omg( z8WSbuQ#yhA7FU*JUe5^%Bn+-S(cQ(<3_&Hnca|q-86l9R5zyG^#!1xC|Q%=U1PyUA_ zyB?{PdqI2no;g&rm`}3no0X&FO$L1w7r%95r#jvk*rp@>W^D3=Unue!ik{t7KZ!v% z`Yrpqis*93&@WuY`c8`%HB*Y2z9;lw+k$(1naGSiO$VCS6@09>^IR*cm)A~2FXxF# z*Fn5?;^s1`L|oL@C)I{U^?1acQ^Xm^qvKIn|AhY2j$Enq-67aS#?M7Hw^7CzcLMm00WVk0KPg+mShjCE4H>HzzHrEylH7+VUQJZc)(%q5}G6wm0 zXe9+6^i^g$IC?SfxSBYB!5HK7C@Fm(lh(==DUTHP?TLiu>pH#RCZm_Cv7Qu~7?>+{ z4={_m`NKB{#0-koCfFa9@8fTt=a#sN2bcn#f_wb!_<)f^ATzA0u z+v+pI2h|arP0w!`6arRh(R4L2PtV1kwW=jmQ(4Fv;meXY`II7bY%hHJ0&RatSP^~k zy>=W<7sjAv`3#|-4b#DRcB=0Zq=44DDH6q8U(w3Vhlt&6B9Tk5wlcVkos(DX#2gV{ zXL;LdT??)OT;8S3D<87KGkZd;Aclg}$CP@<$Ma94Uc2!rWe(@vLI zkh+uABvuRXO!avC+lxmwRIIDVBki6D_w)p(ymy}6EIBR{(DRH;^7|>L3NG{-JMbBt zA@s21B+(6&GS+-`uPJqlB&oV@fOKnC+Qo31MoLu%jT1mG5qZh|c@1h2U>BBU^R613;tN8SFrfDkpO+}TeQ{7BAU?@o_jnu+kJFaYP6=FFTgyr* z+@Us};d)+nT0Z{8mY46DC|~C7%}8QNJ!^8rB@_LPsV{>D*wLlLMLt}>;(BTUq-<>Z6Z5x%b>9gJ&>20iR ziG*FY4~Xt)>FEXEz)xM(xgct@Z{I3P2o6T)YbGnt_CLXsCExcJC;INI^_q{D%ULKL zJ{*(I+sp0al`Be|v9eetVA{kuF{N_iqet?Cnv#+`@c9efQrPpu7a8@bsM!}@sN`~% ze(4TIl?l6<5Ju~t{5d{0_ln9dRxB;ZQui(7Nln5-OnG@Vo2gQ)UrGA!Sp6r}JGSv` zmdkE@t;+RY$=jEokG1nZfs8B1B|x@c|0)%^4+$8y)pvfI34~pn<$z9J!D^WGXm8O! za?nS5;Sk)nF#00G$Z1bk)?GK{0{7|JX_3Fv)4Dvq>19*TXb}>C*NlKQT7%lcvcLSw zG#gXRxFMW7%!-|bg5j<0DK@X$i+ zWyG7i+d+h-1Ss(*#4YA&vtv7EwoI^6*ptz51wC@mqS}}D-!g_gNi-q^G>YSy-pWnW z6!H7&>^)h>Zk)?)se54Tf73J|(ETy1(Ru&(HiYjr3szck;7XEIcO6J29f_&1YEz6c zN4zyF|4pTU=+hO;8n=J~b z#1<#^dw!^oI0sHF&K;qfPWIc4A&?}gn2lKZ_QBek^?A}rmrP6m`9?*>neN95US8MH z5AWZr^C6%re|ObWJXKZw1wk_z>0G`GOj&odU082u@1V%ey4J|}yIq|C;E&M=Zafg* zt#yGTAgZ$R_2Oo#SSPd9-i|Z^>s{ye<#$}S$X(VPJaJdZ@VhY<7IHdi!5@WmWH2ij z&wzFsRb56C+!u#(+6JIVS{f>s!?&aHUF>32!C8r8ru74W^NC$?mp3Xcqbir4d_L;wTUoQoB&?zu0CdN z`iT?yxK4zf&j8Ndn=XzODQsr4{k2?@P(;}$n4k}ZI+M-AgVtOFy2T!o40CP>1+{zh z8phw^V?zMF+e?b0HZ57@t573|_sOe13U^`k#V*^evsvyhQPzh`;<6KKIH;4V{j+}K zp}xp^BFgicC(JboXc%zp?*NAxs5l&XZc1 zOInNy0Nyj<3WU|6$!KsN8-ehbA}9Fys*dhzYH+Q7Sl$W+mQH5X;SDY!S0Za;tX)1# z^_igN>8QDk(`Sv_zjO(n)iU~A&`!?`2SY_KXuOg&HSamOZypVd1*Vvaw_4Sv=%h6s z++4bDzS4fLey(;vTY0{ur!y;Ql5v5ApeW;Usoe8W7iXU4PwUtn-eca$!#>%SA`hkE zPJj#x3kJ{c9R!6kc&1H)ipUcc$Rw7b%h;0E%YjQz_a|bZ(pGw5@zOTQ_0%%{!dTxMwuQjksLq+pPgHG%MzN!Mu{N zoO>$dVW)J@FFCUaRdR%U+^kCZ1xu&IE$~fS*yW8RDmo_lt}mx9$fz@hJ?-X8t;Yl> z=IxiP`<5B;dd4lzH=>6mUhCN*=6{pp!$0fj7MEt;ON;?@^O6VYs7>ZzYXzIT+<)?! z38c86sba|*n5v>XlI{8W+Pn{JHSioGdiK30Vbr|x!M-ap(oyx@hb};zQ5Sm6d(oB- z9*qlbgFziMXYaqr_FZ{Zl38EoIJ(sNR&Z+{a3jB5Y=qqg&Gy)Np;mv)aOQu_Swa8Xg(%nL)(+bdUH|DNCZ$h}Usn9Ys! z&JOA0m2=xg(UQP6tQVO~ZQ_q0M)08=X0<796u9ZFt0ll=rJya9nl4OJ8`KXye#}XD z!}<|vKR^E`ZT*rWR>nlXV!u3|-Tap}gS50zMR33oSnu@L4#N^+)DTXWb?{50pqkrO zC~)!A^@;Sy$MK*;FLd&!AEZ*vxIrQb7D*qJ)7&0~(z}}L-0yYg%wBFj_!4x$0Dr)p>9^{eTWH@wI>s8*ffgc*H=O- zRd#S4*}BYX^l1mn1yDL^SU2G6I{%R;)?fY--Ch^0F`Bb%wrMN4Sq(Hea#OLI{Q6Vd z2jnss^+=B3dt^?wQ}c4%w(m|ufqOM-IyzvXgt`nzb@u~tkomZi? zf_!aU`h!Ij6JG^9Zf+Cq0sX9I^>_qh#%S6r$#w;{7Zx4ze3TkjpEU4@1f2FYpRqFo z3+~pF_mzFA3;JEF&6RU*Z|2rCtd4+AS6AgJF%s?Si}it7*MkvSrKHn@fJ`Pp<%ic> z4+s!XvEqcZI*z?Ka#H&{2E#_iXjbYPpTlo$gAD5GnnHUgS#R?^vE|K2c{dUzR$>{ zvR5v^q?(qBG;_;&WhV(yXAZ$6iyF0GAp)fn-?fLk+G=0WUPGNf#Ym9Frg)x4p;l-E z^x%VW{#kHuHXV`pNRxMV%F3VXN?9}n*B21Lf|r;uaKF`XFdeE&?_6v4oHr&S=ANE8x&(#&FA6HgtckS7kM4|30RTo z`Na2Jf_u)~AN9xoEWj~q4CemG{rjKR=|A7QmIA&^7&6$^I_$%^Te|LUmt>BXk_zj3 ztIN(*o(jl(>h}5ex7((iPnN}32Le7ZxjCs-vt<>i12*fJf7q`^1wtBlz3cK&AlaI^5R zrL~c*% z*kzYt+Q(A>bsDG58{d^ftR+ZTU_k%{op*D1T+*tyC;eR9XA`Gt3r=@qI;m8lShri4 z5ZHADM9?}bw6EcCSa8OCz9E)aAR~3l-$HKTjXuHzQkO{Gk`uv~p z6>ry+AC7_-%_ZhatmS}84Z-}SMma4uhprTg%jf$)M~LD4^VuD@CnUr&PY}jD|6<0&{?#Iv&I=a=?NhU)fr`{+sz=_|E}wM0`iT>0;ACdcyW z*boJ^Pvbp6qrnJnHFJNVxvMG=tI960KmhB9s#Scyv1t#n0%6CgtwfEFSH=YssT?N$ z^0n5E9IE8JhU!*TM#nQ%8i6Y*m8Q#+RHi&jx3j#d%^JSjgE@r1I6>ArAC4!@Irb&&Pw5O zDhJqRe|{P~x8ij6iwX2oDxJ@t1mF+@sA6WKM!Y%WH&UVu?O66Z&y4&R(lY7MgHV00 zwopH|#@?X6auJ}(QVpND+Lazh61XwQ$$GfGX_$)Nxbx7oC){wmj6S2YN$`l_P7ZdW z%>fFOM{?E@IHD~KcalknH~mg~ZV#@fqxf!KoJR@epf*_(%QXhq#U2H zQ+{po+}k|JX|Y@Ebf4?#H}EIH)~JhDpshnbbbIhhM~bRS(a!^{XB1 zYCk)Bl?9tmjEN$^T#?dA7PGVW$tx;`N8%F_+<5zDNyQn&D?HxUtt@G;XzVO*Eo4NR zkNwzZ$s!cIJb*)v-YE_LG6q30hUaK8s6yo7^oxM?Ro{%<0tQytcmf1 z&6C21r?8-t%Apw7L`yoEI@`sYCfm6KDLDA-88&d=?ljYq^iWlED4=DS;$vkMSXg-5^bD@S73;~Y7)E9M_K*MW=V~qZEK&`!urfhh4Gaui zwCp-OJ~1)%pjYwco5Ww078*$t} z!bAlQIAhfxLrt0dB~34GuSa< zW{{IGpIB!Zp8@tRwfp~763gp+P*r&R~`=!fJf{GVH zR|*~NM1L5tMe5Bmdh9E)Ggim}JOaB0i;O_HPJ9K$$u)fc-t({5HDzU;PO=;Oo~_g9 z=f6LE#P+v#-|u!9Sq&U`=}{# z=JR$SEK)eZIZh6%dd*l9Av+Y2~t{xhUG`L)Og z`;pkE%J8vIc@IpqQQ8M(5kL7i4>m>m554-w53G>mGUcMQwX^#sNMYV~7#S$^vH4%Q z0ejaH18?`?Ad6V~d8~-+WKNvy0d|4de(XV<{rpa5tb_3&Nb7r)!~aq0HNZo|_z?&M z!Y!nP*<$Z=`h;Cl66a3mN5BL7NIkE1&K>O1{xdPoJc)Oh2W682mSLP&TqQjd*MGTi zm^df6B6NdK=x zlqR%gj1n6M0tT?KOd$ft{|W;ZPv#*?&r}}5iDgu`EchMl-x3;yaO*n0@o+70CNlm9 zH*G+LGr#;4cNM$*aZ00qXOaF1s3+Dlz<%ay?WcZbSi$(Wc3PZyNEEn-V`xSmp!VrF z{_~Xtw%Y*9;A(5>P}aOsEIYE_O#1Ff-TS|<&6=IT@6LJ{@DfWXYYB*BTnQK}OsxzP zELCzX`JmJ_=FmG;g*?dj&d$!NM$RK^56k{Gb9 zBy})+w}dMfN188b7XKC)`}z7W*G{}m3~)0D3><7fI~%Y%`XTOuXXTve34Orv%$lOL z`L8aPReiq!ri~T^(CrusLnBbQ?;|y5IS!DZSWne;sc&0pV$T!@V=KtcWe&$>usyL9 z2K(aUP@c%{Ha;q9LSuGi)ndP$phN+@94v=)9_S-RzqWlZzIeh%^RqpU1_<^oo_pqq zEu$WvVt)@DGM7rWj$ekObTKz;lm4YINF2(W7|OX%zBlqZoCUOv1_R<*03A;to|1Z* z03kZ+x&|d#e|C_l&;;jD#_Gs{t>^(vM&IaD`Z+ryDeCuCT3TaRA=N?yU%4mLI@Fqf z0exWww1e?hHn~b}u!8kgv1ako-ii_*u9X4_L?whqx)R0myhxcC-hWP+m$t;jD>Kg{ zV1GQG+Cg^f()nx@g;fYI<8F!P=uB%jW;&1;Y6Z$v~Y&UEIJj!zv2EB|zTlOF25O8^5pwf4=-J znMM)o{raY=sFYQ}FHnUB_JQLax3M=7lb{+OuigA|oKO zQ(KUc6pRV5_``ttvW&VAWH>0(GV(P?%F6ka_DA?U-=FF8hIa%Sw)stAGeZe-TbX{& zn0&evvk-qsql?t-**|$Ij^th<^c7$GH0t*c>&`Qco&HG_B z6@fUjHJ2U#uI^{P+eZ&(ydT2uFe1mFc)izjnc^;J<>$Fw5HH~o!&2tz_#s|fP z0iH5K%*ro}t}mYh>BX(Iv%m77{ZW8Gao>qy$7)`&+QjCvGe<!nS))NRpXX>kb~+gk#{E1w-UUwd!O!{oSwy*c|4PH`md zjJW1_)|s#UqvxGE9QTXc+(&3>R^N>X7S=hp9$|SgR|6YfO zJ;J^Ccpq69jgVK9W>(|_Z5(B;)^g1mL5N6;zbVyaySOD7@diyj3R;fhw1MD)KzQSB z2DPtcDenq00Rb{UYMTgz>{MU7rTd`|U~jdTMl2FW)1P4^<2b{%ouFOwe_d z*5*qdmEM(HB=R)rW#;m|nEoQw=`Yn3QeDy5x|>L6sA8LzppcH+4g!8MYKLCJ?tJmz zmXOA&+A)YAm&3EHmg#N$B#ni=-@wRwpQT5zeUD^wJA|9fOtWUnaa0>qUrg9Kwo`$F5JqXlU9Og!ghm+D|_3*m+%{5vA!d?<10K5z_ScE9Tt4R5?{v^CLYXuOhlB78HY(e&9TNEy-YCn$Mn($v;hAOTew6vBy9Ro3!|cF zO84(vwrzI{X!zG>7ULX4ztiQNR^nSf!ZSsFyEuNrmiN!PsZTy_Bw(<0|4~WoOmW#6 zS5@EB)1_q!-5b#3`&j??b%4cMwGSB4H@76g*Js$6FH`pkUXhn2-j`lK0j&2wdKB8B z*t`d{c>~X3s2-`dl5^?1gdyhn-u-1c?6mqA^}g;TH8WF8j3kxaZv5&O;2?PhQM;6p zuIF%eKjz4L=h%3Hp2=U4Y~^~EEE1DH_mNug^Q619Tw_R+H6mMn*u#8XPif^Fmo#DG zHibpZGXV?bjYW)MCeo@z>m78WcU4N>_U|EEtlptiik=bgb5)rxnc%-a%6F0jn{wx0 z1$u3T7hrNpF6L8^d)Xv2J=&}^j}<_k78NZDWimf(c3?b6%YvGhcyB%F#XftrOz34S zu*$yoh;Vx}!IZ-IyO}~iGQBxsR%0;P*em-tlUd5>?8t@*=5(=V=@ne?#gBogN1-Wx z%s27S=0D41vsBz&*+PFq``K<51T*{_zF2w4rq7D9`(Hh|TOE~^9(X9#&hF&KfS6hg z0M(*oiIpQZdihC_QO}+A6PF8e+g4MnSJ(4}QAi7V7!YKjrDe>!4GCq@Zbw4b zSC7lENzT*x#eyTPdFY$L@MYZ0f_uJ>V@y1VF3Rz{Y#XzM9Qii(Kt#61a}T;9o|+W* z&7kA?umq5#4F>LZ))hPUr&IJ~iG;@sN_&%iv*l+zU)eHmqca>I2l9cmYJJ~G5OmVI5*L@l zVChu?(5%H=6|Y4-7B?r(B>sVAfjAK5u`0i$bOb*Fvk8ON%6L0nP>bVV8kV{tsrL|baE^Y-ZVeuy5F^cx@7+H&RP^vOOAHx# zc9=x9={JwncLZE>wybU%Dss?JM35@OB?4GffKF@jGC0z;T|YCRUuHXi^w+Lt^3S1MNx1a1&uSBQ76vgb>EVIG zaXNN)`+8?@BRTZ3r!1lm3<#<-nqj#{i5z&cH3I1w%C+)f(7ZTUoxu^Po zY*`m~94DIUiMbgzz-Woft5*g8FBahCRckw*Cv&D<9&(~{M!%0rpZslnq~lwc0b@D- z9D##o#e#t2e5Fjchccy8QQN62(xWPAJV^AZD;BrOEIgpgw`|B3n|~n5N`pPZvX?9j zbdd4iBuO%Ib{bxJrBAN*=n*;)ZKu?;0|L8&?WEgJBkAO=EWC%KXP{_ZQ|h;7S}l}E zz{#w#85?}rq)MK>p*vEib4sP2Jjs@+XqV9*3ux{g(&ZX0&m;IH9T7ky9fvjXm%FOV05rAFO_senQ{%^B+&pVIqM?Bnb#r?LAXE!!3%(LcFl$=|}t=3gt3?++jk#i0*+UHp(M ztLS@oP016<3uGn4J zyIvm$fj6(Sa@;zwY4eJllr{x+R!qAp;eAZ05`M~o|bYk-wij4B&0qMA6 zUzklZgYM-l{EXC+s=#%~ULIfGqlZ;L(UQJG#tN1m9zNp&R%J1@zIaFT zme8$;#C6ASB}Z^5?OpbbqHTvyp1+&=)eFF=EWm?-m9P~@31+c`YiA+uJG-&3&w=%~ z&PWr~)b|!ztb8Q-j8%KTaM1I-mRrL4>YeWFU%6*lSpzC`A1enZv{`*n*Vb=k z`Czq`i}gm%yB|uD)~oFh!a6fprI#|XJ@Mv1=c0}dz0=n+SK!&pjdk&?!HPhMYUot9 z-G2DBd;Dor!5DuXF^)khG|`wmZIq2KGd}Z}>SDzfO`Z7*L zXpqDGSDLM_eEv)K1^9EJrzFaTK~F&QB(^skAFfw&JpNI-3nw6j^S5xDcl^Umk4?{H z3dCDN@mt{w4Ylh1-D;JFgc^7OexW|rh=c+jiWa_{O#Wsum@(NInsY|#uv~9-J38li zHs{5Dc}UZ0f6di_Ro^{@)OfPZI+^JrF+3{i-T)^w85I-*@;YvG~WpbG?zF@dI9Iy$xnnbO608+Bp#_FC;bmy zCwDkVBvAyA?s73yF1`GA*otXV*!wVYERG@YMrSc*SnoxCGX0ZBnJ?M+%!YE)V@gZ^ zUJW>Qw(Y#D)SG+ae*m6xC$d5mPPKE6$<%TTT)H(jUh+JX_!OU*n8f$hP{(=yx_54D z>5!we8nbYh$sVyP_Qv8kK`U3ilvzFA9C(MM_15XNdqtc^~ORH;p8#HGyGxX{)CvjwLnVz3nA8+;f=>{-YKLy*JZL54RWfy zi3*tW&h!M04y>T|eyH)kWw$W5*fQVT)W}YwqfQp`J^&5=2)tb;R_uIAylqRJSEKz@ zHPm<}DULmLuHm(0CAK^&DCpKcoolz2Ajcvf#mGJ^7w$SJX052CP^9_z4d-2HjcYua z_s^fce9n2djR00px(D1KCFlI4G(9D(=&~ke&TC60lPIeUGl&sltgIGOC6qTFXR|X zDW|NYWoI)|&k)|Qo0PcDHJB_uDRg&lY-~$64;^$!Opf8pPo2$AV|?&U%+Qdp>uh#L zN6)3TX8pL?Q}kxly!{6o}#cNWphD1=YTVu0tnt2}YOQtv6&^oHY z&l+`cjWMo!UwEYq{4IPU#$H*VDHs3xcS%!v`f=3+WRkym^^|b1;ijxCim7I8=p_22 z-Wn=!{M$MDK|OhXO~&O(M~QySyDaibikPCZ<6rV?b0pYv@b#H<;Coha$nlqs^^>zp ztD0Kp-`9)1u0Qq)To6{2bhTJF<{X;dim6`P(l#4EF(ka=p;UbL380ix1F^h|5H%Sa z8`kTf>Nd%&g??G-6#t_4jl@OOuhaPPo1HzE+^!yKZhC3L z-eE~o-__f$WZ&FO+i06vd^<0wDx+}-E1uMYV(yl)slhxVW2@F>2l@LU?#^A4|=L3$jbj0=f%Ioe>=2%P~pJ->ceO49po7h)9>UFr3nPpt~4y zD&JLkq;+-!u}hpBB|n_~iMbJ_lp6&R(PxWqVm!mP1J0%)RdWxR!UkM5Z&RwYt+o;- z{ELE@Zv8~CBI2VGt1$!g*FCMvG3qwhb&d5yxJ9CT@mB?eC5o=JZ@h%wH@F8C-0%Lz zD)io8riObvq*ID{Z%UG@f^ppZi+J{BJ1+X1dA1w`{te~v{5KE@xpD@ zc600d6vOoVw1ZJ2@4RVG_sZQl;+XORJ*)Z=gCfy)u%7s`VAHI_nbWK^f#=D+&u zYy3iP;^0*^!f)m)ftd9!ItOm%`Gahkx{!$t(dr%gvtd-aYAT_TL2krqAj?)dXiW3< zU|VB-7|EXNWjhy-5`E44%}12Rwch|?w*SsqEQ{_NZrVC-YlT?$3^1nl6qb~xm*=kP2{+?dx~kdAIh!Es{X{`j?^#okJy3L|z4y4MjnoA+U7!lC(kRy{D?3O+B&D~GBFvgmanYcLtYgpSY`Q(Yh$P6vb(Zi zjxZO(tbiWJ)A1@b~wB^&+i$hZb9@bL!Ud0d;k)|Mf`U>%-hKL{mV?~gg;({6s3^mF z;Coag7$pB9kF?%sDM41iaIrMp&aQ00l231G#JU1;^?s;7#tybkU5@|$p6k)kaH@#@ zrsP={T{rx3Ph#U1l6SVw$;ngGJ*3AnCJK@!YBNcED(&82%+i*-ot$n#c6gmRZ(zunIy7W z-M-;C$ox}vK`h&76^2`_PL_ZfCnoX*3}m4uGX3N<+WJIr=dG2Pb<(IWswYZE<7(;6 zyC25bcTe(RI5eW{DA^-!Eu)N)j8B+ zugz0=G(-+M-@LI^pEK>dKPHl%G8b4nnWD=Tlh+)Cns4uwQX~Uri;GET7>&b+1|E^cCCB8+A)VTO z;t>9ls`vWYX{93a-x~mZ_B9@fQuDm$QF8}ojF64l-OoQY?G}ML&roaiN;)-FMso#E6X%VNm zST=t?!W*-f4!6AS|Iq*7^Dt%pipz<6lhBxU;$sF$|FhQ!Dimuhq1W_RsvxL`^ISwH z3S`CB^_o@^5i>q@nD6K1-%y@+bV<(Zwd#kv@b`FS;^b*(Zlvm-_~J*dD13^xtT$$7 z4vBtybuYyO%O!lZm{v7bZVlDD8zVedO2i)~Y zQ6wXUc3vd`o|Y8vqSXKQWSRoDa44!;Al&5LTz3rLyfLo&X4!2+1E z;DV{IpI^uRg2i+bTD$ZntQkVOdy9nMexeGyNQxBPh;@A>!2WPwp~H32(?K{R=ycjt zHA7fG$rm6c1HuVk3v!?0)}^oBtBaf!gumkBAYS0Lt(kXu_Uu{UsdR_3k`lwI>kTas zd}8`(*Lb~^A}yR2mdV|j3%bp`LW3V*8B6y0B*Z2V$5lUBJoy2j4Za1?E=$Z``+DlG z@e!sTN4=N-El7%>Szm`-u8AN8vO58)r~Q0{?(g*4y}x@*n222p@Kaj9Cuvu#|0FsT%KlZYyl7xsiI{Nsxzh}AW^CWpzB!LHNpa(aMI2*qt@0zPV@ z`SX(r1*7Lqe^-JRd*T&q0N2*Et5A&A(abM=Q2T70GtBR9I~jvqgZOKY@)Ad_vgv{? z%EBUJA&-AZrC$SA!97C0TtWi(3?)2wZSCzDbN-Ktm=?wjrIHe|UM!jS@LQNDohe07 z3XT+{ZJHaT*JQyFqtozGE2Uow5oe10%*NsuT?{Fr8oVUhocQIh_3u!v1|&SHB$~(Z z^AE>L9WHovoaVc5JpcqZ`-n%sLv;V=`d%w4gSCFCyDgdT>%?ateRG#8A#Sx_dbc~S z2|lKNVY4n!lKC|54L7#YYwuovoZ@w-E6#`TDpxBbl3Z1KRQ1jw6&w-^HO ztZMZQaCMCrQDKuKGz{6;q1ld*(NoXU6H|KLmy-9aYm?l9+gu;1m&_}MO`NqF3GYsn z)iEMx(2SB}`73Xtn!L&jJ`UxTJp20>dKWNf4X_>YY@YSZPf_WOPfg>I;-E#j%EWiF zkE@ZF7b})-ML%jzFQB+*F85v=#Wp9A=OFpq*QktwyusSqTLSApRNQj_vO+xSa) z!*4%NP+@AU>XvNpK+pA|h3RpiPpra}u7Kxi2t$Uxk%$8J&}yXJp@)(l64#j4*-x~2 zNTXSx#A*L{FtCyEA2yMJl#%6j2S+rc;^i~_vzb6vZH!`y+HI-((teb z8vVpVhYZl;4-#nb$Ek@9Z0cp% zUjeCX7paKri<=ZF&>pPZHCd1pu#Kda&Bn%Ajcz`3BwPhpWw>aPo>7Retu-Gbl zYr=Bd=e-Yzwb(x8%yRCUu=U?>R5?gj;&sx08t9QB#AS@u1X2%0mRp}+R(aS%qhE0Y zYzPeRE-btVx~A7ZIVZJzp_F4tJDLLSR*aJKw~ant!D#v#DGE!o%KKj!Y-T_|pD8w~ zX#8AHR@TiQ3Tn}>x1Mq^QKh>9I}k7teEVMUHhw522-50arnf8}htnWh_`UJ^~A-hDRYpDCFz}JaVi4tnu zrlzZ7smkF(dSKo;U4czK2RDL10PY}ooGkcb%9tI-jo3G<<>=97xQMcr<{eE9GW}#; zh7Qilb$El|E%q zDmFiV1CKdieQ+>YsM#PV+3_NG`q$rtkWY;f{oWkYRjO(UMFm(7I4{6-0d(7We{0A6 zS2x`qxO%>N|6rW8P3HA{VC=g+N;Fu8J+F`EZ|vdt(GboJb&-KEIy(r5wc37<;I9O| zGZs-e&YHO#g_-WVo~uB)as4(cZ*1 ztZB=UV6#uXrGBe$Q_i?>qY5Cv%imA&@T>F4S?yBz7(__t z(j%Lx;7uX@n&-;tsu>`s#uLcYkF~4LP~ODOTY7Pc5BEo%g4Z0nnfh!)ns? z1*nmc_@z$o+1X#y%gbsN%r5cM$4kUl~Oi|CymcY|c+pYTVg!<+5%p=Zw?o?yTQf)w54DN6CTM zDu^){6vm7gS1RgfO?jSb8N1}_ml!z1>W zY&oY9i0@-FDXS+1{98or^5J+Ul zOy%WwS`j1cs2<&xDXl)1&sS#;0E5?OT^}i9?%=T+T09^a&mcx}l40vawLFfpOxDnA0o-HyY8qQCfdguVwLE-s`+bq$$&# z)YY{7yD$@onHk)}U=dwIZP{F3Uifusqc)%72^C4+m_A4~L!6sej7PKj*!@K!6*Y*i3MAHH(rwt$ zFJ89_D$3Rmh1{sJc-Go^1^l2?9B3cGf!eGabf3FMpZk3`IEqcCK zp3ygWq4HY;Rc#=_RpinQ>>MxzyiGX5v(^qGy`M!1wWxJSuX01$>UGsxTl}4H!g4r1 z5kGZn+Tv*Q$1leRT9no4DGoc&=MQ(YBL=l*1%JHineI~VI{TbZtfs0I)kuwr?;e|& zog3FPcHMvJm_o`7mMMmLB#&~Siq=8h`WR)HO@PlZ@`?&?w$qHO7YjD;F#4c_iuFMSTu#BDXp@F*#uWyF!Xdn|WqA zE&c^5{mAe77=`Xh_oa<5$fzEgFN1A$d2QKR^P|QCRrM;r`F*&G*V!Vf#>bPL@_WF) zyjtlq;BPfdLh8#Ah)QIusel42i}j#tI-eO%VQ&|yFu$is_KxLtHJSQ$M^Kn65e+dfW5YX zXI%hxv?cR+gE@2^%A&7XuZdCilGC3h16i6hRCUzWb|z$}X1-0xm;%cXFm;3eVq^%! z*f^ME?4&WW@8`0qtF?T$Gp4S<-CLCso}(7g1KG)T+xDDNCi9g9r5%;ad1IWjIugxE zpo&2rJ@NWo40tR6;H0q5ot@*b1)I9)sr^=9+VeK2)^fGtb^Xe&lWmowbC0!(whXp` z8ft8-GV7z$qoLnd-4CHetEojBuWkG~8!hEBuN?7XOvxIoGI2;_-n(bfkJ2?77j@J% zH}U-O^-lCUOhtpnr$-!OBd3HeeQeg#W_ccOlre}xX+kF!ggaTLkJebNE$OcoW|@ie zX$jU=Nu+)pB6n)q{$A<`)|vaJZ>#HajJG5f^iSH8FH4{AFE0r$a>Wf06(YgaqHc$o ztXxIipVyWd+r3IoM%gwyU-iJdwjQll7=ghC>sxje<-iTEE&>(H3b0K?{oz}{&CrBj zkVzG-z+Srd6IoSESXmo`8DO=&x{$-I_om{34D9V!_g{Zv!o!FE__53wcy>^x(>KP$ z@{N?KXhXwR}4Y=c)Nd_2?M+Bbly}muoDKR5Mz&}cvCt7xjgc=x5FG|+` z6QI5J!{f-+x<6C<Q|7JDF#*&m3;; zwmHR;$uzC`9D|yB#^?fD=ITJ0@a}$)ow-7Och05Sbq2#Gg=T8)jrA`tz*+n zCnhYyrGD6uS@Q0LfQiL^ZYH!$Z)X3=vZ3j`$#lRLUYxMoK-uR_LYvi5Ck{{cTQKX5WAx;yz5o@sVLWWR z8hrU3zdLi99CQJLR%Rb6g1=6#TgQJ&=vm#GrNKWy>gK^lsVla}S{llE#(6#L!{HIFs z$BgsW>EFLW1f~9Ld*BYx)ua?IvGiit>x+!;)`D7HW8TZ>w939Ir+ubLRjrG`J1lx6Mtt=Pk~qY$*9m z0LaX0^eX;^AV?k5#@QE6P-s%!jzmI+J6S!?`e)U4rFhMi_d0>;D-+#k`ff#q`Ybk> zK}Yyke96tUDHaMM%vN#1(h(jNTqEN)#Vb+DpkVCaH4E2>8kgbkegx66!$duP>e=+&g zC0wVC%X6;^E#-$-BHpkaB1;{Ac;qmmCnV^O)g@qZ*C2;h7^O2#8rIq@7rAXtOsk*d z)dH}-uT;u5Za>d2;zxixt!qa|!5=zctG{_CPZ{q+t}9ftbdBPtMHBPO^$U zwDS!9PP^fFOBs6JBOmL~rwkE2;^vSmv|`(kMC({Z%Z9vyL3eFS?Oh*y1art!$^c;h zM=RG|VP#WAo@RB$e}M%LKa-gJiMx={F_G8YmPNRCamV=rC^Y(N?N~0wL>w%?TRPR) zX#NY1kEr~W8b@DEUey*KG52seHNVYiAvCq>gd{d@;NjX4#7KTh;N#06HGTbXbJf1y zhD@GbQ6{dRoSC4$UdUBI^uY9p7mK zIE9$YNS;2;i6SBg{<7v2oyMD02FwsuZCeKGoeEUaq>q`LGvCLy3{uht9`xZ%nomYr zTKIgj2OvQw_9^yp88xbGM;5UGE|=R6NKMZVf5H$~bGfK6sE&^G(M}FS;0EEc>6jin z50B5LX?2OQ+;1&k`kzJR^+dG^Jof)Zc>dvPL{ss9&S?9Cd7xSS>dPTbta4RCetRhfe^DtzmK$f`CiK$>?=U8+S zrR(Wh!cp38+FJg#b}}J5(V@8qz+pY`L=?AH^^DD-{(xDfC!La~vOb^S&EiSYW+&&N z!`Y0)BnESj6CI|ZyGNb*OiB-RBcDq?G0tMnS>Oyn+-bhsi3Ubm%kv$jMk>Q1*s5Hy zTHoj4bARRXmJBXZs5{A|T6Xdhxnsim)RC(Lh{S_EUJM-cLH{^gRKu+-RP*)g5SGN< zD#GITNA6x89M8T}n|%r0_n=OlkU0C(chT~bb2cl6uuPji#z`*q*>h{9<7hcS*?ib) zi|x@P%hGD|iK0p!svU@w`=Meq#cq+TVZ-@+**L{n)bmT};{c z*=8H0JADixly5;rfuMu*@q4Mkr=n!Rva)`D-KW`?2h~9s@+a7d~dJCdYT^f<~*=TquAQ6l*2E<#B`e7%#k8@4!zdaQ5& zAENNX*?tuIb8$OyE`gQUO)DPdy5ncJjV;{h?`H?hXX1IVqk*h$Z?641oAAg2BE*;i z%+Q)pCFANPmobqRw!%qr;=-7fyD%rblSbM0!pOnjpORUDo+vz5R=q>#0z0aF#6k91 zyvA^?R7bv0JuS`l_$oc7zGd|{-z`1%EY`wC!SzDb{wQhJlF9V!l)O{-#K#l9L57%j z>C0mzQkRFw-I{9?5<0*Dc`z`LFL-d}IHaLRP>|MBo5=sLi8M>n)xyu71bL;Y@7fhI znix{B5>l{pnYhz#+ZaAKuRXeSlwuusTIuCwa^aIp{c)M-!<}2AmhD%Ae!R%zy$q*z z6SU0b$n^BZ?B134pA@3|S75Io>&)#3f~GCF-5|u}R_@}TP4~caF39D@>N@kYXI&jN zc)sWJ)o5Dm!7zc)WOfn_lW(t=qm)6woNcVs4~NG~JJRe3FgW5~Sp-BKGSJ(vCbrdN z!}>dWJpQmARn7q}_RLW%S8vFkN7Z%OC54PkxUJ=V?OxVUH@z#}@map!R#n~{+wxO6 zS+V@N_+YhaM1uj#*JJlnPAMT&_cPgCSQc?8BmJ&HtF`M~#H#s`!Zg>Q@yc;&LO5(^ z(<(Eov9sGQGL)=N41h(Bw4QUI)>g7+-202m$C-mN+XBjuPmaCs<3>J9?x}Ro-|3{L zw;`i%!52-uT#ybD9#!r5y5u*HQ_}Z2&*e3C!z7iKax5FFwfU;Uxad zoid23x)kn%fvJ{bf}sHGdO@1JX}q*QXKr;N z@)TRjA5hUcE9iGsSeI6cuirM^G=+txb&QAzUhG4Q-Ue2!{MFswCyaYDb(JGba%#Xt z#QG+|K`dh{efCFOW|kR)&*Gv_tJQr=-u;}VGO553rv9{qBDJ2#WUv}*=ejY!r?5zq z4~vDl^;Yo1q&34F^&%q8;fnJoV~jpQs9LsWZrJ%@Xxh43_KFsWbp9`) zk8cLo36-!n%Os^o6V2jCb5(MfaIB@eI)whLFd;MD^*SXc2mhby`ItAdcqFU{1Q6xi z*R#;7p9+l{;i_&wIP9w?#>T4`!%ydiV%)%Ho7We+v}>w+NzgfOY?^e84fxh5(Aj?H z$DSS^ml!K@=7RG(ghE2 zxbgNxKQab2?Nsk*_(FE)1KYL!x>E6~zQA8`fVdDLV~+q0k%qT^WM}{a*j+Lt2?_DC+^7%_RbNXD$eWc9c-E&SzI=--g{72Twl<# zb=*&)43V`6f2OcXX_+-yZK={b!%CjtUGk36ORpzh3}(`s<^W35Djw=b? zX-bn(9M`=~54H$jXNKB-;GUpUTNA0!{8 zq%g-PrbHu@R}t-3Yt)MVGSeHKH_HJ;xMne(4-wxWR&GQ_gEvZ`uo z=sTp6*c54La@RZhUg)W#EZo`Ro9ZePgro$Q8*s1(6`Rxtjyf(8r&9EG?@h0_nm0Ge z$4zUU=p+9Lz79OiH@)uqOm5-)(}Ma)Xl%@Vea)&L>2Iu@kUyW#?#PNCGkz}M)Q9rF zRMZ;uzgY1|VI320!1N~H^~)R339JyeIt)2d&rPf%zc{h3&=WNq*`m|jvN!a5{aQx( zLo1@Lot2quYFX^)IiU>(IpC+twO-WU<{uDf@1|u~ZO&&{F}^`vu2Fhk&{F3UrI2*4 z{W8dLwP&~gv#r>qOiLLjB<6TIl9X%Du?%)p@N3&Wy9tX$qHr6w8SPj$s@K z??O4^w~?rbh-L%T?wp^U0kl)U-sW_8WmU^R(Vs~OjRMBj>_Qvmf3+Z>LxN>BAF?Y@OYf~!?wfS!kko2IF zT}6{XCH1utd-Ofb07)|kaM;rIT#W9Y=3Ve9pAJN2b1`B^MF5p-M3*uDXAY$Bi&ZW% zed&=HFRz_y*@;!qK|```i)*q`!idrIDEBjSEzP_rW!TtmK&Np8WZ-c;i&8w9Q}Z;O z_?i2E-ojSv>+$M3Vs+`~fCS`(6Ujgk?o?r^%&wI?QgtMy1D7Uv(=2g59yl`kbIeoc^o_=WOzNQGD zdhtz!m^d2lxd*?j8zLpu%?J%w+H@~O(B!kUFi>c3s= z1qt!&;RQ4yP995oa;s0K4P?eN$a`tPfJzRLjG$dCsoBb9&h#KPvlV>ZhS6!%h_U@8|?W zE+7U2+g#W^b_9>1Zp`e{2hg$W0M^PoChaBF{`##; zkN78BJwG~UO15AMKhN0$9eUS zC@V5)K+W`5&Ti+e>kK|^bQvoT$hG3N^4t0#O+*4c?bS0eZ3(NGfcPHE$Y>SCPfckB zbIDWWp;kKFU?--q>!Y4pu>^rUV`4d0kJ$)P!?gv!_y3&cp2E3l5*`g?6~aBoOdey5 z9YK(BcO^D^OCuR0qeT5pyDIjX4lBdSU^tgt&~#55NmeK26AC)}$T;|R%=;GJIH-_OKLX zU1%V#?1`>CJrwyuwq@uAm9@&}5tDcU26c`aX`zPqwl!Qzx;S@{CdCS0FqDScnP46s zahYAmavt$^NBiN{Zjx}w{69>Y+(}~t3>KE|4((v!sTC&P<^~fP_rc<18ULG%X+P(b|B|$)>5{bIL|S8hx|HTE9KNmeN+Ml6y-_=F-@G$B*w1 z2;MKcwkB7$v^O?7rdk2(zoDR${JwExaVMSV^l5b9;e|Afou~cX7aXojZFJVG4Gg|l z0~Sw~17496aJuePyx6vxoH{|k7r*^+D-C7o5%B56L@pV6!u&7(@HH3s_Qf@Bge56q za@vo~YEy=)Ntcm`RMY4}*e#~Jz!^-{EHG8NtOsB)#CB8y9~`GWZ$AAjFlG9s12yI^ z+HC$h7Em$MdaAcq>uMugZihOFHl(@#aWSG#fqHSr6%%yM6^p~bc%zJIZVj*R_#O9| zg08~uX0c9*FYE%r+wV)a;X6*8m0K)VGRys?S|GWeD`Gj&ZL)_y)9G{Kb78z<+G|!a zD7|^?m>!l=G}PXSY3KKse37~1wE623D#0R%4m|l4d&FVNI__W;5`=b*=8M~FvmeL5 zM8Pj44M~rk=B5#=Zy?1M-+Gz{IC--k)|$sy9T3%b3N>}yD%I&~oT$Q0&npl#UtR5j zI&cZ(Am!|{HR#8qLqA}e$&UpGE-aWe622EH_*7B2ZEXNgHxz$@xQgcxkP_qRWf$8f z0piI|`QIheoAPnZJ2kKtvdOVKhQ}sHZ<$FOER{~5beSRyG_?Zg;<`qfzj1u`9w*#; zAtXHO(v66fJp5*ybJ5TX@u4os&p-MM3xX|ok;)qh@QT=MjrQES+_(;0#X#BuPhAbS z$z*vXo@6&SiOo%|eX8rtF=h`23zc(idGReX1;2hhhQFd@fI<{3Tx<^(f26BtCljFK9ZL0m zXzIFGOC(HMEKCzHwoGOa2l(fIV-^|Q+XC|eu7rg`pMn-vF1DeOu!K5*x zya<0I6e0(=gS+TWxsBPxkG#k}3((5q4d2p+UrRIAK>8l6M45ko(=`-$Z8~(lD8@@A zoH2mg<01{ho@9Jnv`GvL>teh*qxw0td=ChhM%}J|8UA@{sx1h)?`mVS9<_;5WrH#3 zOS7FYiXLOSJC3@g!Sl0d^iKmu|Ao2!sN-ONY}b=n(+f2VzZ94kou{Xtu!9Fn_vWGC zZE~rP1w;rF+#b~A)OGRt`Z`u>u5I&bfQH7FX0z-nYROsj`gmrZ;p$KXIDsnFxq0>M z2Q@VKr<$tjaaK<2RkERz;-^@;W>THUR^2hP5b2}t%4yHDWBHHYYwWx6Z}}7*^f7u~ zZ$N&kHNMD$1@i1-lbZI~WeFBtF}wzKKpwWf2@xKI<*>0=z*;;jAuGo|^Uv^+si~8R z4#XMYymRB@QcjFF*&`~M@WMg)!!6eljZjlW;NpkY`yb7Aq)J)}+<%67nFOFK%gVMi z+5>iH^n?7agRXcjOf}{|LdH%{FJ35oGyz?$;aCSV97r$%gV;+1X~1Pq(=b9rf@{0U zP`c3*T{b%U7{^7U41o+OFv0?Wn5Xo$cVu5DSRSaG8o-k9QPI$}I&3Zhp>5UP98p`% z?18nt!p^3qlaacJ`1p8WOmLh=_6@D`fsZ@=7=d$}>*UeR)&sgRB#9V#nWvbHQZ_erSf8#C20Hr6!=EZE;@ zLc>WIBvp3-^M7kGquk(|7#qWkPwycyC-CBH5jHk)&R*;K$-Ml$c2lJs)FcWuJ2^Qv zH#axDcoa8f)}JFXflInvY&;46rOx!)uy{STx*6;{{Z2j>Ux{WtL(ne_*GUgHa6f)G zT#6G;>*wd!`1j`vTRv%Xx|h4By9BT=o#p1{W-$SjQ4)$*!3~6WUwOn@1qNp4*q7Mu z;3n|j`)dvo?s?6&33G~lZ#HID>}`2D_iN_mn47YxDJ&qMIw$4oCX-2H3GO%fJ6-KW zZXTZTp``X}rIei08Iy0TnkKkk>*X`AG`sHYJU9Rz!@E^_1Jok8bN6it8%UxLm*n5l zJ-yED13Q}ACF5Q;Potr^t_EOIcm$+3Bod&$p%*k>AkgDSt_?$F7T98Z&o>Z}o@_9XEF_bJGiVW69C1Yp{T#$Lu9_3jX^`jZeY03b@1;@$ah1p>*jIXOLSB?4#`d`Kz z4h->CWp2A262%MfthtI^nce_G$QTH|y@^KA1mitdS^5s?#CBxYBi&V^b}m|$pK>gl z-`cbUsGdp!uP~+$4h{xa*z`nw!lc}_zI&LY!c3(eO|U7z^Jn;BsyYCD!utWd>QV-v z3`s}Jvd@ezkBx$IP6e5Vj(^!K|HzFxYHY->NVt*ijRGK@fx-0cM$Y_KzFR6Mmr=t* z-jWLqR5Fb9_oNKgOJ2No z^>5&D=oKw*YZDH(zp<4}`XIe66n_7mwx@2_hzcdXbLf3UgD-N7p;%gXcV`!~MWgb$ zwyoPK_F^fz_I^iB3!Q^i=FJT7MAVtRX8Xn^WD-r#w{wlHtSD}o7PEZbhQ2nb{d+j% z1qWs=_EOqIrkAa@U6{y9k?Lq6x;xS&&lG#fWD=9^BmZq38S8%#*LT|1*4VCQQyNrk ziQUG$Yc9N@t)p~yc6yyZV;?#gig^Mb<6+7~Px`$?KgwkY zjl=;}Q9T2`JKeOW2@i2@^4be*R0+0AC%u+MKYAKMRw+mKFVZO!@W*soTH3zeJ|He4 zIW{4Zn4DB@d`}zIfIDxi>~QfID>eS)iIJeCJC&NKS+;E?1RQ03pZ;x$!^O$$q`tl# z^kJBAP=g#$F{hmE)Tt2)xjtG2Sp4ZH=X=zg(@l=yQ>-Dy1B?NA|3aN!IskIDxwXa5${>SbQz%Ib=WJkiM&H!MiU}ZIiyb2%vHw_elIM8(=8i^AoK`KC5w|#X- zDSO0!++Ky_Mdj`t4%sx6kjyMtbx{d#>4;P3) zB4437fwJJYz_Xk+XICiP#QdHvJmSxqCmgpI8>I1H0~8+yKYR`Zu)P`Lb4-Gk(T9i1 zjW?O0@IE}gMg3|e=z5)$Ayxs+DU+1P9&|P@4d@yuBx@)5M)0hYwGg=AVhR+F(ZP^z zORsDHr{O9Q>gjo&#NWh}ls>~?XDOZL8t^!>WoJ41E|dya2^=pj0zh?_i7K$&zj?eu z@Yue~#0k)-;V!##*dT*YcwDZow);|7hV;b5MEQR#2p0a~`=Fguzc5P$4aSVZ&cC#>-ir76JTo8(Rq<#+9VH@kBa1+&7A-~{-054?!LX-NZ!fH!J!0% z1%R`(Kj+;*la2+0wEt^IB3bam9jR;Y=_neMz|B&U<&(oaa=Y9V*EEgRh2J1!ft^xwieV7o=6Lgb4=81Ve=fGH&xHekzA>KH0ZBh*Kmlu<`~v3(%c1cH7p!7p(!CuNy8hk8V*jEwA)! z!S~M10|Guyg_h(ak&9JuhLWVyX}qBxT0G&&1qY_EBE`$fhRs{}H7N&>T1*Ab?KTAydFg)7m<0sxhh+SWC`$ z;h3L1`EN+U3z59dC!-k@gqwzS*bAiD45c$E#ZRq9J&@D5-tPzsTr!*|J^KnwFZf+b zuU~0zZ*ObCDQ{Fp52&>aP+4;2G>~t)#(mQ~l2r!ndb^^r5&cDh5qBYNa_s%M(f+2* zKknY5{))EL!DPUYf)?0@(NW7r`v<8X!((OwhNwuNNag+K4SGy?%qwE_F;G)Kt@r}{ z`l4-j@y5P`$i7wV>>h6K0Onte$jS%=SO(((zDMsZ*$WJIBJGX*z*<9)*%<9AZ@WGS zCI!&r(|hfGH8w!qZ=Wj;D`|~Z-6ALeFd{*x60sl<2rvvOV3@$`$ocleUb=rrSG*9x zDm>jH@nfl|vfUiSD9cY6SRT4 zy`!Vwbv5thId2epV?*;_rM{FU`1a>+!f?Y%eCUL+-|ChMl$OX>$Icl(3tS8c#55A` zwbz#x7b_&u-Z16{W?Vx6)Xmf3FaW?0Uw94nyIlNPiXVZR_$&iQ&F>uS7@A3Mx{>p( z+k8U6g3RK)P9>S!P%>yP^V6qK!POmbxF3LgWDID)EJK=fwKIv%ur~%fi@+nQL~VQA zyLaxk*3Xlgn39#1m5@b4P7aR1G&DA*Bqpb%q%cZ&nh(ugb;nX+PT_YFJ5Nap2d)bY4$aSJyyy zcznFda}nTx7bc5$vr55x_o2e`MiyX$hW|hBUy~Dj`}ZG!MsF4octY>-h;IJ=x% + OpenBot + + + + + + + + + + + OpenBot + Self-hosted GitHub maintenance automation + triage · review · fix · chat — your repo, your keys, your data + +