diff --git a/board/_board.py b/board/_board.py
index cb959ba..7c8e38e 100755
--- a/board/_board.py
+++ b/board/_board.py
@@ -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; '
@@ -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
@@ -996,12 +1012,21 @@ def community_row(e, note_html):
doors = data["doors"]
if doors:
H.append("
🚪 All doors
")
- H.append("every conductor and faculty
")
+ H.append("every agent and workflow door
")
for d in doors:
+ if d["tier"] == "skill":
+ continue
tier = ' (faculty)' \
if d["tier"] == "faculty" else ""
H.append(_row(f'/{esc(d["verb"])}{tier} — {esc(d["desc"])}',
f"/{d['verb']}"))
+ skills = [d for d in doors if d["tier"] == "skill"]
+ if skills:
+ H.append('Workflow doors — compositions and '
+ 'dev-flow entries, no agent of their own:
')
+ for d in skills:
+ H.append(_row(f'/{esc(d["verb"])} — {esc(d["desc"])}',
+ f"/{d['verb']}"))
H.append(" ")
if data["degraded"]:
diff --git a/board/_publish.py b/board/_publish.py
index f0500ec..d9cb49f 100755
--- a/board/_publish.py
+++ b/board/_publish.py
@@ -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)
diff --git a/tests/test_board.py b/tests/test_board.py
index a9d8b08..906ce46 100644
--- a/tests/test_board.py
+++ b/tests/test_board.py
@@ -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/")
@@ -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)