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
31 changes: 28 additions & 3 deletions board/_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,10 @@ def sparkline(history):


def collect_doors():
"""The conductor/faculty roster, read from the dispatcher registry itself
(bin/pyauto-brain is the single source; never a second copy here)."""
"""EVERY door, from the two single sources: the dispatcher registry
(conductors + faculties — all the agents) and skills/*/SKILL.md (the
non-agent doors: compositions, dev-flow entries, ship/cleanup workflows).
Never a second hand-written roster."""
script = (
f'source "{BRAIN_HOME}/bin/pyauto-brain"; '
'for v in "${CONDUCTOR_ORDER[@]}"; do printf "conductor\\t%s\\t%s\\n" "$v" "${AGENT_DESC[$v]}"; done; '
Expand All @@ -445,6 +447,20 @@ def collect_doors():
parts = line.split("\t", 2)
if len(parts) == 3:
doors.append({"tier": parts[0], "verb": parts[1], "desc": parts[2]})
agent_verbs = {d["verb"] for d in doors}
# board (this page) and wake_up (superseded BY this page) stay off the
# roster on purpose.
skip = agent_verbs | {"board", "wake_up"}
for skill in sorted((BRAIN_HOME / "skills").glob("*/SKILL.md")):
verb = skill.parent.name
if verb in skip:
continue
m = re.search(r"^description:\s*(.+)$", skill.read_text(encoding="utf-8"),
re.M)
desc = m.group(1).strip() if m else ""
# First sentence only — the SKILL.md carries the full contract.
desc = re.split(r"(?<=[.!?]) ", desc, maxsplit=1)[0][:140]
doors.append({"tier": "skill", "verb": verb, "desc": desc})
return doors


Expand Down Expand Up @@ -996,12 +1012,21 @@ def community_row(e, note_html):
doors = data["doors"]
if doors:
H.append("<h2>🚪 All doors</h2>")
H.append("<details><summary>every conductor and faculty</summary>")
H.append("<details><summary>every agent and workflow door</summary>")
for d in doors:
if d["tier"] == "skill":
continue
tier = '<span class="muted"> (faculty)</span>' \
if d["tier"] == "faculty" else ""
H.append(_row(f'<b>/{esc(d["verb"])}</b>{tier} — {esc(d["desc"])}',
f"/{d['verb']}"))
skills = [d for d in doors if d["tier"] == "skill"]
if skills:
H.append('<p class="muted">Workflow doors — compositions and '
'dev-flow entries, no agent of their own:</p>')
for d in skills:
H.append(_row(f'<b>/{esc(d["verb"])}</b> — {esc(d["desc"])}',
f"/{d['verb']}"))
H.append("</details>")

if data["degraded"]:
Expand Down
17 changes: 14 additions & 3 deletions board/_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,20 @@ def main(argv=None):
return 2

DEVBOX_FILE.parent.mkdir(parents=True, exist_ok=True)
if DEVBOX_FILE.exists() and DEVBOX_FILE.read_text() == text:
print("board publish: devbox observation already current — nothing to push")
return 0
# Idempotence ignores the timestamp: an unchanged observation must not
# bump the file (and re-trigger brain_board.yml) just because the clock
# moved between two runs.
if DEVBOX_FILE.exists():
try:
prev = json.loads(DEVBOX_FILE.read_text())
except (json.JSONDecodeError, OSError):
prev = None
if prev is not None and \
{k: v for k, v in prev.items() if k != "ts"} == \
{k: v for k, v in payload.items() if k != "ts"}:
print("board publish: devbox observation already current — "
"nothing to push")
return 0
DEVBOX_FILE.write_text(text)

rel = os.path.relpath(DEVBOX_FILE, PUBLISH_REPO)
Expand Down
22 changes: 19 additions & 3 deletions tests/test_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,17 @@ def test_json_surface_is_complete_and_derives_org(tmp_path):
assert s["open_issues"] == 42
# Community section reuses the Ears' scan surface wholesale.
assert s["community"]["counts"]["awaiting_response"] == 0
# The doors roster comes from the dispatcher registry, both tiers.
# The doors roster covers every agent (dispatcher registry, both tiers)
# AND every workflow door (skills/ minus the agents).
verbs = {d["verb"] for d in s["doors"]}
assert {"intake", "health", "vitals"} <= verbs
assert "board" not in verbs # surfaces are not agents
skill_verbs = {d["verb"] for d in s["doors"] if d["tier"] == "skill"}
assert {"route", "prm", "start_dev", "issue_cleanup"} <= skill_verbs
for d in s["doors"]:
if d["tier"] == "skill":
assert d["desc"], d["verb"] # frontmatter description parsed
assert "board" not in verbs # the page never lists itself
assert "wake_up" not in verbs # superseded BY this page
# Sibling boards resolved against the pages base.
assert s["boards"]["heart"].endswith("/PyAutoHeart/")

Expand Down Expand Up @@ -512,10 +519,19 @@ def test_publish_commits_and_pushes_to_main_only(tmp_path):
assert shown.returncode == 0
assert json.loads(shown.stdout)["worktrees"][0]["repo"] == "RepoA"
assert "hygiene" not in json.loads(shown.stdout) # --no-hygiene
# Re-publishing an identical observation pushes nothing new.
# Re-publishing an identical observation pushes nothing new — and the
# comparison ignores the timestamp by design (an unchanged observation
# must not re-trigger the board just because the clock moved).
r2 = subprocess.run([str(BRAIN), "board", "publish", "--no-hygiene"],
capture_output=True, text=True, env=env, cwd=tmp_path)
assert "nothing to push" in r2.stdout
state_file = brain / "state" / "devbox_board.json"
stored = json.loads(state_file.read_text())
stored["ts"] = "2020-01-01T00:00:00Z"
state_file.write_text(json.dumps(stored, indent=2, sort_keys=True) + "\n")
r2b = subprocess.run([str(BRAIN), "board", "publish", "--no-hygiene"],
capture_output=True, text=True, env=env, cwd=tmp_path)
assert "nothing to push" in r2b.stdout
# Off main, publish refuses (guard against feature-branch commits).
subprocess.run(["git", "-C", str(brain), "checkout", "-qb", "other"],
check=True)
Expand Down
Loading