From 82e93149f40c37545a6b2eb4fb906cbd16b82e35 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 17:22:53 +0000 Subject: [PATCH 1/2] The Hands board joins the family look MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release board carried its own copy of the GitHub-grey stylesheet: a plain h1, dark-only, GitHub-blue links, a blue version pill — none of it the orange the logo above the README is drawn in. It renders through the Brain's `board/_theme.py` instead: the hero (the palm and gear mark, the wordmark, the hairline-and-dot rule, the tagline), the accent as link and heading colour, and the shipped version as the verdict banner the family already uses. The theme is imported, never copied — release_board.yml and tests.yml check PyAutoBrain out beside this repo, and a local run resolves the sibling checkout the way the other PyAuto tools find each other. `--md`, `--badge` and `--json` never touch it. The per-row status dot and the version chips stay here, written against the theme's variables so this board follows the family accent rather than setting a second palette. The copy chips move to the family's delegated handler (`data-cmd`, one shared script instead of a per-page one), and the footer becomes the family chips — each sibling board in its own organ's colour. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FhsBsESQUAoJQ2fvLiDD5j --- .github/workflows/release_board.yml | 7 ++ .github/workflows/tests.yml | 7 ++ autohands/board.py | 158 ++++++++++++++++------------ tests/test_board.py | 16 ++- 4 files changed, 117 insertions(+), 71 deletions(-) diff --git a/.github/workflows/release_board.yml b/.github/workflows/release_board.yml index 392fd3b..cb5c287 100644 --- a/.github/workflows/release_board.yml +++ b/.github/workflows/release_board.yml @@ -41,6 +41,13 @@ jobs: steps: - uses: actions/checkout@v4 + # The family look (hero, palette, stylesheet) is defined once in the + # Brain and imported by the renderer — never copied in here. + - uses: actions/checkout@v4 + with: + repository: PyAutoLabs/PyAutoBrain + path: PyAutoBrain + - uses: actions/setup-python@v5 with: python-version: "3.12" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2919508..dfa99eb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -91,6 +91,13 @@ jobs: with: repository: PyAutoLabs/PyAutoMind path: PyAutoMind + # The shared board look the dashboard renders with — imported from the + # Brain, never copied here, so the board tests exercise the real thing. + - name: Checkout PyAutoBrain (the shared board theme) + uses: actions/checkout@v4 + with: + repository: PyAutoLabs/PyAutoBrain + path: PyAutoBrain - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: diff --git a/autohands/board.py b/autohands/board.py index 6c97512..8c07dd2 100644 --- a/autohands/board.py +++ b/autohands/board.py @@ -35,6 +35,7 @@ import datetime import html as _html import json +import os import re import subprocess import sys @@ -43,6 +44,36 @@ from pathlib import Path HANDS_HOME = Path(__file__).resolve().parents[1] + +# The family look lives once, in the Brain (``board/_theme.py``): the +# stylesheet, the hero that redraws this organ's logo as a mark, and the +# cross-board footer. Imported rather than copied, so the look moves for the +# whole family at once — release_board.yml checks PyAutoBrain out beside this +# repo, and a local run finds the sibling checkout the way the other PyAuto +# tools resolve each other. +BOARD_KEY = "hands" # this board's entry in the Brain's palette table + + +def theme(): + """The shared theme module, or a RuntimeError naming the fix. + + Only the html path needs it; ``--md``/``--badge``/``--json`` never call + here, so the digest keeps working with no PyAutoBrain in reach. + """ + for cand in (os.environ.get("PYAUTO_BRAIN"), HANDS_HOME / "PyAutoBrain", + HANDS_HOME.parent / "PyAutoBrain", + Path.home() / "Code" / "PyAutoLabs" / "PyAutoBrain"): + if not cand: + continue + board = Path(cand) / "board" + if (board / "_theme.py").is_file(): + if str(board) not in sys.path: + sys.path.insert(0, str(board)) + import _theme + return _theme + raise RuntimeError( + "the shared board theme (PyAutoBrain/board/_theme.py) is not in reach " + "— check PyAutoBrain out beside this repo or set PYAUTO_BRAIN") CONFIG_PATH = Path(__file__).resolve().parent / "config" / "workspaces.yaml" SCHEMA_VERSION = 1 @@ -275,11 +306,14 @@ def _heart_board_url(snapshot: dict) -> str: def _boards_nav(snapshot: dict) -> str: + """The cross-board footer — one chip per sibling, each in its own organ's + colour (the theme owns the chip palette; this board owns the URLs).""" owner = str(snapshot.get("owner") or "").lower() if not owner: return "" - return " · ".join(f'{name}' - for name, repo in BOARD_FAMILY) + links = {key: f"https://{owner}.github.io/{repo}/" + for key, repo in BOARD_FAMILY} + return theme().boards_footer(links, BOARD_KEY) def _latest(snapshot: dict) -> dict | None: @@ -358,16 +392,45 @@ def _render_md_brief(snapshot: dict) -> str: def _copy_btn(payload: str, label: str = "copy") -> str: + """A one-tap payload chip. The behaviour is the family's shared script + (``_theme.JS``): a delegated click handler reading ``data-cmd``.""" return (f"") + f"data-cmd=\"{_html.escape(payload, quote=True)}\">\U0001f4cb") _PYPI_CLS = {"live": "ok", "yanked": "fail", "missing": "warn", "unavailable": "unobs"} +# The lede, and the page-specific shapes the shared sheet has no opinion on: +# the status dot each row carries and the version chips. Written against the +# theme's variables, so this board follows the family accent rather than +# setting a second palette. +_LEDE = ("What the Hands shipped — a record of execution, newest first. Tap " + "\U0001f4cb to put a command on your clipboard for a Claude Code chat.") + +_EXTRA_CSS = """ +.chip{display:inline-block;background:var(--btn);border:1px solid var(--line); + border-radius:999px;padding:.15rem .6rem;margin:.15rem .2rem 0 0; + font-size:.85rem;white-space:nowrap} +table.recent td.dot{width:10px;padding-right:0} +table.recent td.dot::before{content:"";display:inline-block;width:10px; + height:10px;border-radius:50%;margin-top:.35rem;background:var(--muted)} +tr.ok td.dot::before{background:var(--ok)} +tr.warn td.dot::before{background:var(--warn)} +tr.fail td.dot::before{background:var(--bad)} +table.recent td.name{font-weight:600;white-space:nowrap} +.ndot{display:inline-block;width:12px;height:12px;border-radius:50%; + margin-right:.25rem;background:var(--muted)} +.ndot.ok{background:var(--ok)} +.ndot.fail{background:var(--bad)} +.errors{margin-top:1.2rem} +footer{margin-top:2rem;color:var(--muted);font-size:.82em} +""" + + def _render_html(snapshot: dict) -> str: + t_ = theme() latest = _latest(snapshot) head = (f"{_html.escape(latest['version'])}" if latest else "unavailable") head_age = _age(latest.get("date")) if latest else "" @@ -422,74 +485,31 @@ def _render_html(snapshot: dict) -> str: errors = (f"

unavailable this " f"render:

    {items}
") heart = _heart_board_url(snapshot) + hero = t_.hero(BOARD_KEY, "Dashboard", _LEDE) return f""" -PyAuto releases — {head} - - -
-

PyAutoHands Dashboard

-

{head} {head_age} markdown version

-

What the Hands shipped — a record of execution, newest first. - Whether it is safe to release lives with the - PyAutoHeart Dashboard.

-

{chips}

-

📋 copies the command into your clipboard, ready to paste - into a Claude Code chat.

-

Libraries

- {''.join(lib_rows)}
-

Release train

- {''.join(train_rows)}
- {nightly} - {errors} -
Rendered by autohands/board.py from the GitHub + PyPI - APIs · generated {_html.escape(str(snapshot.get('generated') or '?'))} · - Hands executes, never gates.
-

Boards: {_boards_nav(snapshot)}

-
+PyAutoHands Dashboard + + + +{hero} +

{head}{head_age}

+

Whether it is safe to release lives with the +PyAutoHeart Dashboard — the Hands execute, never gate. +markdown version

+

{chips}

+

Libraries

+{''.join(lib_rows)}
+

Release train

+{''.join(train_rows)}
+{nightly} +{errors} +{_boards_nav(snapshot)} +
Rendered by autohands/board.py from the GitHub + PyPI +APIs · generated {_html.escape(str(snapshot.get('generated') or '?'))}.
+ + """ diff --git a/tests/test_board.py b/tests/test_board.py index e468137..ff13ad6 100644 --- a/tests/test_board.py +++ b/tests/test_board.py @@ -69,7 +69,7 @@ def test_shipped_date_comes_from_the_version_scheme(): def test_failed_train_run_carries_a_bug_prompt(): html = board.render(SNAP, "html") - assert "data-copy=" in html and "cp(this)" in html + assert "data-cmd=" in html # the shared copy handler's payload hook assert "/bug Release train: SomeHands release.yml run failed on 2026-06-01" in html assert "https://ci.invalid/runs/1" in html # the action chips are present too @@ -112,7 +112,7 @@ def test_html_is_self_contained(): assert "fetch(" not in out and "XMLHttpRequest" not in out # data-copy payloads are inert clipboard text, not asset loads — strip # them, then every remaining URL must sit in an href. - stripped = re.sub(r'data-copy="[^"]*"', "", out) + stripped = re.sub(r'data-cmd="[^"]*"', "", out) for m in re.finditer(r"(?:http|https)://", stripped): before = stripped[max(0, m.start() - 30):m.start()] assert 'href="' in before or "href='" in before, f"non-href URL at {m.start()}" @@ -132,3 +132,15 @@ def test_boundary_language_links_the_heart(): for out in (md, html): assert "Heart" in out # readiness explicitly deferred to the Heart assert "https://someorg.github.io/PyAutoHeart/" in html + + +def test_html_wears_the_shared_family_theme(): + # The look is the Brain's `board/_theme.py`, not a stylesheet copied in + # here: the page must carry this board's hero (mark, wordmark, tagline) + # and its accent, or it has silently fallen out of the family. + t = board.theme() + html = board.render(SNAP, "html") + assert t.MARKS[board.BOARD_KEY] in html + assert t.ORGANS[board.BOARD_KEY]["tagline"] in html + assert t.ORGANS[board.BOARD_KEY]["ink_dark"] in html + assert "#58a6ff" not in html # the old hard-coded GitHub blue From db4f5bb5927ff215c4174e19944bc43da9b1e610 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 17:34:58 +0000 Subject: [PATCH 2/2] Resolve the Brain checkout without naming the org MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tenant firewall caught it (PyAutoHeart#169 CI): the theme resolver's last candidate was a literal `~/Code/PyAutoLabs/PyAutoBrain`, and the org's own directory name is an instance fact — organ code may not carry one, whatever the local convention happens to be. The fallback reads `$PYAUTO_ROOT` instead (defaulting to `~/Code`), the same declared surface the dev-flow doors use. `$PYAUTO_BRAIN` and the two checkout paths are unchanged, so CI and a sibling workspace both resolve exactly as before. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FhsBsESQUAoJQ2fvLiDD5j --- autohands/board.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/autohands/board.py b/autohands/board.py index 8c07dd2..849b394 100644 --- a/autohands/board.py +++ b/autohands/board.py @@ -54,6 +54,16 @@ BOARD_KEY = "hands" # this board's entry in the Brain's palette table +def _workspace_root() -> Path: + """Where the sibling PyAuto checkouts live: `$PYAUTO_ROOT`, else `~/Code`. + + The org's own directory name is an instance fact, so it is never written + here — a workspace that does not follow the default sets `$PYAUTO_ROOT` + (the same variable the dev-flow doors read). + """ + return Path(os.environ.get("PYAUTO_ROOT") or Path.home() / "Code") + + def theme(): """The shared theme module, or a RuntimeError naming the fix. @@ -62,7 +72,7 @@ def theme(): """ for cand in (os.environ.get("PYAUTO_BRAIN"), HANDS_HOME / "PyAutoBrain", HANDS_HOME.parent / "PyAutoBrain", - Path.home() / "Code" / "PyAutoLabs" / "PyAutoBrain"): + _workspace_root() / "PyAutoBrain"): if not cand: continue board = Path(cand) / "board"