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..fcc5a04 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,46 @@
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 _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.
+
+ 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",
+ _workspace_root() / "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 +346,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 +443,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 +456,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 +615,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
-
-
-