Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <what you want>` 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
Expand Down
31 changes: 31 additions & 0 deletions agents/conductors/intake/_intake.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<summary>` — HTML, not markdown.

Expand Down Expand Up @@ -1091,6 +1114,10 @@ def render_dashboard(c: dict) -> str:
"<details>", "<summary>Headerless prompts</summary>", ""]
L += [f"- `{h.split(' — ')[0]}`" for h in c["hygiene"]]
L += ["", "</details>"]

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"


Expand Down Expand Up @@ -1372,6 +1399,10 @@ def h2(title, src):
H += [record_row(r) for r in rows]
H += ["</details>"]

boards = _board_links(home)
if boards:
nav = " · ".join(f'<a href="{_attr(u)}">{n}</a>' for n, u in boards)
H.append(f'<p class="muted">Boards: {nav}</p>')
H += [f"<script>{_HTML_JS}</script>", "</body>", "</html>"]
return "\n".join(H) + "\n"

Expand Down
53 changes: 41 additions & 12 deletions board/_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -701,17 +713,33 @@ def render_html(data):
f'<b>{counts["awaiting_response"]} awaiting our reply</b>. '
'Replies stay human-gated in <code>/community</code>.',
"/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'<a href="{_attr(url)}">{esc(e["repo"])}#{e["number"]}</a>' \
if url else f'{esc(e["repo"])}#{e["number"]}'
H.append(_row(
f'{link} <span class="muted">[{days} waiting]</span> '
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'<span class="warn">[{days} waiting]</span>'))
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'<span class="muted">[{note}]</span>'))
for e in c["awaiting_review"]:
H.append(community_row(
e, '<span class="warn">[review requested]</span>'))
else:
H.append(_row("Scan unavailable — run the Ears directly.", "/community"))

Expand Down Expand Up @@ -762,7 +790,8 @@ def render_html(data):
H.append(_plain(f'<span class="warn">{esc(d)}</span>'))

nav = " · ".join(f'<a href="{_attr(url)}">{esc(name)}</a>'
for name, url in data["boards"].items())
for name, url in data["boards"].items()
if name != "brain")
H.append(f'<p class="muted">Boards: {nav}</p>')
H += [f"<script>{_HTML_JS}</script>", "</body>", "</html>"]
return "\n".join(H) + "\n"
Expand Down
5 changes: 5 additions & 0 deletions config/policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions tests/test_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}</a>" in footer, name
assert ">brain</a>" not in footer # a board never links itself


# --------------------------------------------------------------- read-only --


Expand Down
Loading