From dcd1e2ce65bddf10e37b12a09e28b15a6a68f889 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 17:22:39 +0000 Subject: [PATCH 1/2] The Memory board joins the family look MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The knowledge board carried its own copy of the GitHub-grey stylesheet: a plain h1, dark-only, GitHub-blue links, and one violet pill that was the only trace of the logo it sits under. It renders through the Brain's `board/_theme.py` instead: the hero (the book and node-tree mark, the wordmark, the hairline-and-dot rule, the tagline), the accent as link and heading colour, and the count strip carrying pages, cited, to-cite and queued. The theme is imported, never copied — knowledge_board.yml and validate.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. What the shared sheet has no opinion on stays here — the citation bar, the collapsible queue sections, the paper filter, the read-rate sparkline — but written against the theme's variables, so this board follows the family accent instead of setting a second palette. The copy chips move to the family's delegated handler; the one thing that needed saying is that a chip inside a must not also toggle its section. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FhsBsESQUAoJQ2fvLiDD5j --- .github/workflows/knowledge_board.yml | 7 + .github/workflows/validate.yml | 7 + scripts/board.py | 221 +++++++++++++++----------- tests/test_board.py | 16 +- 4 files changed, 157 insertions(+), 94 deletions(-) diff --git a/.github/workflows/knowledge_board.yml b/.github/workflows/knowledge_board.yml index 2e5a26e..1a0f677 100644 --- a/.github/workflows/knowledge_board.yml +++ b/.github/workflows/knowledge_board.yml @@ -43,6 +43,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/validate.yml b/.github/workflows/validate.yml index f504345..7cf7b86 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -10,6 +10,13 @@ jobs: runs-on: ubuntu-latest 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/scripts/board.py b/scripts/board.py index 671eafd..fbf11ae 100644 --- a/scripts/board.py +++ b/scripts/board.py @@ -38,6 +38,7 @@ import datetime import html as _html import json +import os import re import subprocess import sys @@ -46,6 +47,36 @@ MEMORY_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 — knowledge_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 = "memory" # 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"), MEMORY_HOME / "PyAutoBrain", + MEMORY_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") + KEY_MARK = "**Canonical BibTeX key:**" STATUS_RE = re.compile(r"^status:\s*(\S+)", re.MULTILINE) WIKILINK_RE = re.compile(r"\[\[([^\]]+)\]\]") @@ -305,11 +336,14 @@ def _repo_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 _read_prompt(snapshot: dict, section: str) -> str: @@ -399,8 +433,7 @@ def _render_md_brief(snapshot: dict) -> str: def _copy_btn(payload: str, label: str = "copy") -> str: return (f"") + f"data-cmd=\"{_html.escape(payload, quote=True)}\">📋") def _bar(statuses: dict) -> str: @@ -413,7 +446,74 @@ def _bar(statuses: dict) -> str: return f"{seg(reviewed, 'ok')}{seg(drafted, 'mid')}{seg(stub, 'lo')}" +# The lede, and the page-specific shapes the shared sheet has no opinion on: +# the citation bar, the collapsible queue sections, the paper list and its +# filter, the read-rate sparkline. All written against the theme's variables, +# so this board follows the family accent instead of setting a second palette. +_LEDE = ("What the organism knows, and what it still owes a citation. On a " + "paper: \U0001f4e5 intake · \U0001f4d1 cite · \u2705 mark read — each opens a " + "prefilled issue (add notes, submit to act). \U0001f4cb copies a Claude Code " + "prompt.") + +_EXTRA_CSS = """ +.bar{display:inline-block;width:90px;height:8px;border-radius:4px; + overflow:hidden;background:var(--btn);vertical-align:middle; + border:1px solid var(--line)} +.seg{display:inline-block;height:8px;float:left} +.seg.ok{background:var(--ok)} +.seg.mid{background:var(--accent)} +.seg.lo{background:var(--muted)} +details.qsec{border-top:1px solid var(--line);padding:.5rem .25rem} +details.qsec>summary{cursor:pointer;list-style:none} +details.qsec>summary::-webkit-details-marker{display:none} +details.qsec>summary .name::before{content:"\u25b8 ";color:var(--accent)} +details.qsec[open]>summary .name::before{content:"\u25be "} +.name{font-weight:600} +ul.papers{margin:.5rem 0 .25rem;padding-left:1.3rem} +ul.papers li{margin:.4rem 0} +ul.papers li.done{color:var(--muted)} +a.act{margin-left:.35rem;padding:.05rem .4rem;font-size:.85rem; + border:1px solid var(--line);border-radius:6px;background:var(--btn)} +a.act:hover{background:var(--tint);border-color:var(--accent); + text-decoration:none} +details.hist{margin:.25rem 0 .25rem 1.3rem} +details.hist>summary{cursor:pointer} +#pfilter{width:100%;padding:.45rem .6rem;margin:.25rem 0 .5rem; + background:var(--btn);color:var(--fg);border:1px solid var(--line); + border-radius:8px;font:inherit} +#pfilter:focus{outline:none;border-color:var(--accent)} +.spark{display:inline-flex;align-items:flex-end;gap:2px;height:12px; + margin-left:.45rem} +.spark .sb{width:5px;background:var(--accent);border-radius:1px; + display:inline-block} +table.recent td.name{white-space:nowrap} +footer{margin-top:2rem;color:var(--muted);font-size:.82em} +""" + +# The shared copy handler is delegated, so a chip inside a would +# also toggle its section. Swallow that one default; the filter is this +# board's own behaviour and stays here. +_EXTRA_JS = """ +document.addEventListener("click",function(e){ + var b=e.target.closest("button.copy"); + if(b&&b.closest("summary")){e.preventDefault();}},true); +function flt(q){q=q.toLowerCase(); + var secs=document.querySelectorAll('details.qsec'); + for(var i=0;i=0; + li.style.display=hit?'':'none'; + if(hit){any=true;if(li.className==='done')histHit=true;}} + if(q){d.style.display=any?'':'none';d.open=any;} + else{d.style.display='';d.open=false;} + var h=d.querySelector('details.hist'); + if(h){h.style.display=(q&&!histHit)?'none':'';h.open=!!q&&histHit;}}} +""" + + def _render_html(snapshot: dict) -> str: + t_ = theme() t = _totals(snapshot) pct = round(100 * t["resolved"] / t["sections"]) if t["sections"] else 0 repo_url = _repo_url(snapshot) @@ -505,97 +605,34 @@ def _render_html(snapshot: dict) -> str: f"{_copy_btn('/memory ' + w['name'], 'copy: recall this domain')}" f"") + hero = t_.hero(BOARD_KEY, "Dashboard", _LEDE) + stats = t_.stats((t["pages"], "Pages"), (f"{pct}%", "Cited"), + (t["todo"], "To cite"), (t["queued"], "Queued")) return f""" -PyAutoMemory — {t['pages']} pages - - -
-

PyAutoMemory Dashboard

-

{t['pages']} pages · {pct}% cited markdown version

-

On a paper: 📥 intake · 📑 cite · ✅ mark read — each opens a - prefilled issue (add notes, submit to act). 📋 copies a Claude Code prompt.

-

Reading queue ({trend_bits}){spark}

- {filter_box} - {''.join(queue_blocks)} -

Citation work queue ({t['todo']} sections need a canonical key)

- {''.join(todo_rows)}
-

Sub-wikis ({snapshot.get('bib_entries', 0)} bibliography entries · - {snapshot.get('links', {}).get('wanted', 0)} wanted pages)

- {''.join(wiki_rows)}
-
Rendered by scripts/board.py from the checkout — - nothing here is committed; generated {_html.escape(str(snapshot.get('generated') or '?'))}.
-

Boards: {_boards_nav(snapshot)}

-
+PyAutoMemory Dashboard + + + +{hero} +{stats} +

markdown version

+

Reading queue ({trend_bits}){spark}

+{filter_box} +{''.join(queue_blocks)} +

Citation work queue ({t['todo']} sections need a + canonical key)

+{''.join(todo_rows)}
+

Sub-wikis ({snapshot.get('bib_entries', 0)} bibliography + entries · {snapshot.get('links', {}).get('wanted', 0)} wanted pages)

+{''.join(wiki_rows)}
+{_boards_nav(snapshot)} +
Rendered by scripts/board.py from the checkout — +nothing here is committed; generated +{_html.escape(str(snapshot.get('generated') or '?'))}.
+ + """ diff --git a/tests/test_board.py b/tests/test_board.py index bb621c2..ca48b82 100644 --- a/tests/test_board.py +++ b/tests/test_board.py @@ -90,7 +90,7 @@ def test_contents_level_only_no_body_text_leaks(tmp_path): def test_work_queue_prompts_reference_the_documented_workflow(tmp_path): snap = board.collect(_tree(tmp_path)) html = board.render(snap, "html") - assert "data-copy=" in html and "cp(this,event)" in html + assert "data-cmd=" in html # the shared copy handler's payload hook assert "reading-queue.md" in html # file-the-next-paper prompt assert "make validate" in html # every prompt ends at the gate assert "/memory demo" in html # the recall chip @@ -164,7 +164,7 @@ def test_html_is_self_contained(tmp_path): assert out.lstrip().startswith("") assert "src=" not in out and " 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 --- scripts/board.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/board.py b/scripts/board.py index fbf11ae..fcc5a04 100644 --- a/scripts/board.py +++ b/scripts/board.py @@ -56,6 +56,16 @@ BOARD_KEY = "memory" # 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. @@ -64,7 +74,7 @@ def theme(): """ for cand in (os.environ.get("PYAUTO_BRAIN"), MEMORY_HOME / "PyAutoBrain", MEMORY_HOME.parent / "PyAutoBrain", - Path.home() / "Code" / "PyAutoLabs" / "PyAutoBrain"): + _workspace_root() / "PyAutoBrain"): if not cand: continue board = Path(cand) / "board"