From c75db15161cd218f56b1937c5b5a8a1f9d38579b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 16:45:33 +0000 Subject: [PATCH] =?UTF-8?q?brain:=20board=20follow-ups=20=E2=80=94=20READM?= =?UTF-8?q?E=20dashboard=20paragraph,=20per-conversation=20community=20chi?= =?UTF-8?q?ps,=20cross-board=20footer=20everywhere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README now follows the organ pattern: one intro paragraph, then a 'See the PyAutoBrain Dashboard' paragraph; the command-surface prose moves under How PyAutoBrain works. The board's community section renders EVERY open external conversation as its own row with a one-tap /community triage chip — awaiting-reply first with days waiting, then ours-to-watch and review-requested — not just the awaiting list. The cross-board footer nav becomes a family affair: config/policy.yaml board: boards: now carries all six one-tap boards (brain included) as the single source; the Brain board and the Mind dashboard renderer (_intake.py, stdlib regex on that block — the refresh workflow installs nothing) both render it, each skipping its own entry. 415 tests pass; firewall gate green. Co-Authored-By: Claude --- README.md | 18 +++++----- agents/conductors/intake/_intake.py | 31 +++++++++++++++++ board/_board.py | 53 ++++++++++++++++++++++------- config/policy.yaml | 5 +++ tests/test_board.py | 39 +++++++++++++++++++++ 5 files changed, 125 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a35386e..3862235 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,21 @@ classifies each task, plans it, and routes it to specialist agents — and it delegates everything else: it holds no state (the Mind's job), runs no health checks (the Heart's), and never releases anything itself (the Hands'). +See the **[PyAutoBrain Dashboard](https://pyautolabs.github.io/PyAutoBrain/)** +for the organism's morning and general starting point: what ran overnight, the +Heart's readiness headline, who in the community is waiting on a reply, what +to resume, and the upkeep doors — each actionable row with a one-tap 📋 +copy-for-Claude command. Regenerated each morning; the local sync/clean leg is +one terminal command, `bash bin/morning.sh`. + +## How PyAutoBrain works + You drive it in plain English, through short slash commands in a Claude Code chat: `/intake` to file an idea, `/start_dev` to begin a task, `/health` for a check-up — or just `/route ` and the Brain picks the right door. The full command surface (13 conductors + 5 faculties) is the generated table in [AGENTS.md](AGENTS.md). -Start the day on the **[Brain board](https://pyautolabs.github.io/PyAutoBrain/)** -— the organism's operational dashboard, regenerated each morning: what ran -overnight, the Heart's readiness headline, who in the community is waiting on -a reply, what to resume, and the upkeep doors, each with a one-tap 📋 -copy-for-Claude command. The local sync/clean leg is one terminal command, -`bash bin/morning.sh`. - -## How PyAutoBrain works - 1. **A task arrives.** Usually from the Mind's backlog — pick a task on the [PyAutoMind dashboard](https://pyautolabs.github.io/PyAutoMind/) and paste its `/start_dev` command — or free-form, via `/route` or any conductor's diff --git a/agents/conductors/intake/_intake.py b/agents/conductors/intake/_intake.py index 9d05fdf..9206add 100755 --- a/agents/conductors/intake/_intake.py +++ b/agents/conductors/intake/_intake.py @@ -728,6 +728,29 @@ def _pages_url(home: str) -> str: return f"https://{m.group(1).lower()}.github.io/{m.group(2)}/" if m else "" +def _board_links(home: str) -> list: + """The cross-board footer nav every one-tap board carries: (name, url) + pairs for the sibling boards, from PyAutoBrain's declared config surface + (config/policy.yaml `board: boards:`), skipping this page's own entry. + + Stdlib regex on the one-pair-per-line block, not yaml — this renderer + also runs bare in PyAutoMind's dashboard_refresh workflow, which installs + nothing. The owner comes from the census home, so no org is named here. + """ + m = re.match(r"https://github\.com/([^/]+)/", home or "") + policy = Path(__file__).resolve().parents[3] / "config" / "policy.yaml" + if not m or not policy.is_file(): + return [] + owner = m.group(1).lower() + block = re.search(r"^ boards:\n((?: \w+: \S+\n)+)", + policy.read_text(encoding="utf-8"), re.M) + if not block: + return [] + pairs = re.findall(r"^ (\w+): (\S+)$", block.group(1), re.M) + return [(name, f"https://{owner}.github.io/{repo}/") + for name, repo in pairs if name != "mind"] + + def _summary_label(value: str) -> str: """Task text rendered inside a `` — HTML, not markdown. @@ -1091,6 +1114,10 @@ def render_dashboard(c: dict) -> str: "
", "Headerless prompts", ""] L += [f"- `{h.split(' — ')[0]}`" for h in c["hygiene"]] L += ["", "
"] + + boards = _board_links(c.get("home", "")) + if boards: + L += ["", "Boards: " + " · ".join(f"[{n}]({u})" for n, u in boards)] return "\n".join(L).rstrip("\n") + "\n" @@ -1372,6 +1399,10 @@ def h2(title, src): H += [record_row(r) for r in rows] H += [""] + boards = _board_links(home) + if boards: + nav = " · ".join(f'{n}' for n, u in boards) + H.append(f'

Boards: {nav}

') H += [f"", "", ""] return "\n".join(H) + "\n" diff --git a/board/_board.py b/board/_board.py index a9b5b9d..372d39d 100755 --- a/board/_board.py +++ b/board/_board.py @@ -502,11 +502,22 @@ def render_md(data): f"{counts['open_external_prs']} external PR(s) open — " f"**{counts['awaiting_response']} awaiting our reply** " "(respond via `/community`; never auto-reply)") + awaiting_keys = {(e["repo"], e["number"]) for e in c["awaiting_response"]} for e in c["awaiting_response"]: days = (f"{e['waiting_days']:.0f}d" if e.get("waiting_days") is not None else "?") - L.append(f" - {e['repo']}#{e['number']} [{days} waiting] " - f"@{e['author']}: {e['title'][:70]}") + L.append(f" - `/community triage {e['repo']}#{e['number']}` " + f"[{days} waiting] @{e['author']}: {e['title'][:70]}") + for e in c["open_external_issues"] + c["open_external_prs"]: + if (e["repo"], e["number"]) in awaiting_keys: + continue + note = ("ours to watch" if e.get("awaiting_response") is False + else "unchecked") + L.append(f" - `/community triage {e['repo']}#{e['number']}` " + f"[{note}] @{e['author']}: {e['title'][:70]}") + for e in c["awaiting_review"]: + L.append(f" - `/community triage {e['repo']}#{e['number']}` " + f"[review requested] @{e['author']}: {e['title'][:70]}") else: L.append("- scan unavailable — run `/community` for the live surface") L.append("") @@ -536,8 +547,9 @@ def render_md(data): L.append("## Degraded") L += [f"- {d}" for d in data["degraded"]] L.append("") - L.append(f"Boards: " + " · ".join( - f"[{name}]({url})" for name, url in data["boards"].items())) + L.append("Boards: " + " · ".join( + f"[{name}]({url})" for name, url in data["boards"].items() + if name != "brain")) L.append("") return "\n".join(L) @@ -701,17 +713,33 @@ def render_html(data): f'{counts["awaiting_response"]} awaiting our reply. ' 'Replies stay human-gated in /community.', "/community")) - for e in c["awaiting_response"]: - days = (f"{e['waiting_days']:.0f}d" - if e.get("waiting_days") is not None else "?") + + def community_row(e, note_html): + """Every conversation gets its own one-tap triage chip.""" url = e.get("url") or "" title = esc(e.get("title", "")[:80]) + kind = "PR " if e.get("type") == "pr" else "" link = f'{esc(e["repo"])}#{e["number"]}' \ if url else f'{esc(e["repo"])}#{e["number"]}' - H.append(_row( - f'{link} [{days} waiting] ' - f'@{esc(e["author"])}: {title}', - f"/community triage {e['repo']}#{e['number']}")) + return _row( + f'{kind}{link} {note_html} @{esc(e["author"])}: {title}', + f"/community triage {e['repo']}#{e['number']}") + + awaiting_keys = {(e["repo"], e["number"]) for e in c["awaiting_response"]} + for e in c["awaiting_response"]: + days = (f"{e['waiting_days']:.0f}d" + if e.get("waiting_days") is not None else "?") + H.append(community_row( + e, f'[{days} waiting]')) + for e in c["open_external_issues"] + c["open_external_prs"]: + if (e["repo"], e["number"]) in awaiting_keys: + continue + note = ("ours to watch" if e.get("awaiting_response") is False + else "unchecked") + H.append(community_row(e, f'[{note}]')) + for e in c["awaiting_review"]: + H.append(community_row( + e, '[review requested]')) else: H.append(_row("Scan unavailable — run the Ears directly.", "/community")) @@ -762,7 +790,8 @@ def render_html(data): H.append(_plain(f'{esc(d)}')) nav = " · ".join(f'{esc(name)}' - for name, url in data["boards"].items()) + for name, url in data["boards"].items() + if name != "brain") H.append(f'

Boards: {nav}

') H += [f"", "", ""] return "\n".join(H) + "\n" diff --git a/config/policy.yaml b/config/policy.yaml index 8b94a3d..92a50d5 100644 --- a/config/policy.yaml +++ b/config/policy.yaml @@ -113,8 +113,13 @@ board: - PyAutoLens:autolens/__init__.py reference_release_repo: PyAutoLens heart_board: PyAutoHeart + # The full one-tap board family (the cross-board footer nav every board + # carries; each renderer skips its own entry). Parsed by board/_board.py + # (yaml) and by the Mind renderer's footer (stdlib regex on this block — + # keep one `name: Repo` pair per line). boards: mind: PyAutoMind + brain: PyAutoBrain heart: PyAutoHeart hands: PyAutoHands memory: PyAutoMemory diff --git a/tests/test_board.py b/tests/test_board.py index a74a166..ca88245 100644 --- a/tests/test_board.py +++ b/tests/test_board.py @@ -276,6 +276,45 @@ def test_apply_writes_the_four_pages_files(tmp_path): assert badge["label"] == "brain" +def _community_item(repo, number, login, title, comments=0): + return { + "number": number, + "title": title, + "user": {"login": login, "type": "User"}, + "html_url": f"https://github.com/{repo}/issues/{number}", + "labels": [], + "comments": comments, + "updated_at": "2026-07-10T00:00:00Z", + "repository_url": f"https://api.github.com/repos/{repo}", + } + + +def test_every_community_conversation_gets_its_own_chip(tmp_path): + stub = _fabricate(tmp_path, _default_fixtures(**{ + "comm_issues.json": {"items": [ + _community_item("ExampleOrg/RepoA", 7, "some_user", + "lens model crashes"), + _community_item("ExampleOrg/RepoB", 9, "other_user", + "docs question", comments=2), + ]}})) + page = _run(["--html"], tmp_path, stub).stdout + # One 📋 triage chip per conversation — awaiting-reply and watched alike. + assert 'data-cmd="/community triage ExampleOrg/RepoA#7"' in page + assert 'data-cmd="/community triage ExampleOrg/RepoB#9"' in page + md = _run([], tmp_path, stub).stdout + assert "`/community triage ExampleOrg/RepoA#7`" in md + assert "`/community triage ExampleOrg/RepoB#9`" in md + + +def test_boards_footer_lists_the_family_without_self(tmp_path): + stub = _fabricate(tmp_path, _default_fixtures()) + page = _run(["--html"], tmp_path, stub).stdout + footer = page[page.rindex("Boards:"):] + for name in ("mind", "heart", "hands", "memory", "organism"): + assert f">{name}" in footer, name + assert ">brain" not in footer # a board never links itself + + # --------------------------------------------------------------- read-only --