diff --git a/bin/README.md b/bin/README.md index 9215d8c..d2a83a6 100644 --- a/bin/README.md +++ b/bin/README.md @@ -6,6 +6,10 @@ Executable tooling for the PyAuto organism. - **`install.sh`** — the cross-organ Claude/Codex skill and command installer. - **`check_skill_line_counts.sh`** — the skill line-count **guard** (primary skill `.md` files must stay < 200 lines). +- **`heart_feed.py`** — "Heart at the door": reads the Heart's published + `state.json` (fallback: local `pyauto-heart readiness --json`), read-only, + never ticks; exit 0 green · 1 yellow/stale · 2 red · 3 grey. Run first by + `start_dev` step 0a, `start_bundle` and `route`. The installer and guard used to live in `admin_jammy/skills/`. They moved here because they are organism-wide infrastructure, not admin_jammy's own tooling — diff --git a/bin/heart_feed.py b/bin/heart_feed.py new file mode 100755 index 0000000..f8c17fa --- /dev/null +++ b/bin/heart_feed.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +"""bin/heart_feed.py — the Heart at the door (read-only, fast). + +WHY. The Heart gate used to be consulted only at ship time, so a session could +plan and build a whole branch on top of a RED organism and only learn it at the +end. The human's rule is "fix Heart before development"; this helper is what +`start_dev` / `start_bundle` / `route` run FIRST (step 0a "Heart at the door") +so the verdict is on screen before any plan is made. + +WHAT IT READS. The Heart's published organ-cockpit feed — `state.json`, the v1 +contract in `board/_state.py` — at `//state.json`. The +heart board comes from `config/policy.yaml` (`board: heart_board`) and the pages +base from the Mind's body map (`board/_board.py`'s `repo_homes` / `derive_org`, +imported, never re-derived); `BOARD_PAGES_BASE` overrides the base, as it does +for the board and the vitals faculty. No org is named here. + +FALLBACK. When the published feed is unreachable or unreadable (offline, a +blocked network, a malformed publish) — or with `--offline` — it asks the local +Heart CLI, `pyauto-heart readiness --json` (PATH first, then the sibling +checkout's `bin/`), and maps that verdict into the same shape. + +WHAT IT IS NOT. It never runs a Heart tick and never refreshes anything: a +reader at the door must be fast and must not mutate state. The vitals faculty +remains the live, authoritative read at ship time. + +Output: `Heart: RED — (updated 2h ago)` then one line per red/yellow +item (info items too when the status is not green and nothing is red/yellow). +`--json` prints the same summary as one JSON object for the skill seams. + +Exit: 0 green · 1 yellow/stale · 2 red · 3 grey/unreachable. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import urllib.error +import urllib.request +from datetime import datetime, timezone +from pathlib import Path + +BRAIN_HOME = Path(__file__).resolve().parents[1] +POLICY_PATH = BRAIN_HOME / "config" / "policy.yaml" +TIMEOUT = 5 # seconds — the door must never stall a session +FALLBACK_TIMEOUT = 30 + +STATUSES = ("green", "yellow", "red", "stale", "grey") +EXIT = {"green": 0, "yellow": 1, "stale": 1, "red": 2, "grey": 3} +SEVERITY_ORDER = {"red": 0, "yellow": 1, "info": 2} + + +# ----------------------------------------------------------------- source --- + + +def _board(): + """board/_board.py, imported lazily (the URL derivation lives there).""" + sys.path.insert(0, str(BRAIN_HOME / "board")) + import _board # noqa: PLC0415 + return _board + + +def heart_board_repo(policy_path=POLICY_PATH): + """`board: heart_board` from the policy, by a stdlib regex (no PyYAML — + the same one-pair-per-line parse the Mind renderer uses on this block).""" + try: + text = Path(policy_path).read_text(encoding="utf-8") + except OSError: + return None + m = re.search(r"^\s+heart_board:\s*([A-Za-z0-9_.-]+)\s*$", text, re.M) + return m.group(1) if m else None + + +def feed_url(): + """(url, why) — the published state.json URL, or (None, reason).""" + repo = heart_board_repo() + if not repo: + return None, "no `board: heart_board` in config/policy.yaml" + base = os.environ.get("BOARD_PAGES_BASE") + if not base: + try: + board = _board() + org = board.derive_org(board.repo_homes()) + except Exception as exc: # noqa: BLE001 — the door degrades, never crashes + return None, f"cannot derive the pages base ({exc})" + if org is None: + return None, "cannot derive the GitHub org (no body map, no git remote)" + base = f"https://{org.lower()}.github.io" + return f"{base.rstrip('/')}/{repo}/state.json", None + + +def _reason(exc): + try: + return _board()._fetch_reason(exc) + except Exception: # noqa: BLE001 + return str(exc)[:80] or exc.__class__.__name__ + + +def fetch_feed(url, timeout=TIMEOUT): + """(feed, why) — the parsed, sanity-checked feed or (None, reason).""" + try: + with urllib.request.urlopen(url, timeout=timeout) as resp: + obj = json.loads(resp.read().decode("utf-8")) + except (urllib.error.URLError, json.JSONDecodeError, ValueError, OSError) as exc: + return None, _reason(exc) + problem = feed_problem(obj) + if problem: + return None, f"served a feed that is not state.json v1 ({problem})" + return obj, None + + +def feed_problem(obj): + """The one reason this cannot be read as a Heart state, or None. + + Deliberately lenient: the full contract is `_state.validate_state`, but a + door that refused a RED feed over a missing `pages_url` would hide the RED. + Only what the door itself prints is required. + """ + if not isinstance(obj, dict): + return "top level is not an object" + if obj.get("status") not in STATUSES: + return f"status {obj.get('status')!r} is not one of {'/'.join(STATUSES)}" + if not isinstance(obj.get("items", []), list): + return "items is not a list" + return None + + +# --------------------------------------------------------------- fallback --- + + +def resolve_heart_cli(): + """`pyauto-heart` on PATH, else the sibling checkout's bin/ (the same + order as agents/_common.sh `resolve_heart`). None when neither exists.""" + found = shutil.which("pyauto-heart") + if found: + return found + try: + sys.path.insert(0, str(BRAIN_HOME / "agents")) + import _pyauto_root # noqa: PLC0415 + from _repo_paths import repo_path # noqa: PLC0415 + root, _ = _pyauto_root.workspace_root_reason() + heart_repo = heart_board_repo() or "" + if heart_repo: + cand = repo_path(root, heart_repo) / "bin" / "pyauto-heart" + if cand.is_file() and os.access(cand, os.X_OK): + return str(cand) + except Exception: # noqa: BLE001 + pass + return None + + +def from_readiness(r): + """Map `pyauto-heart readiness --json` into the state.json shape.""" + verdict = str(r.get("verdict", "")).lower() + status = verdict if verdict in STATUSES else "grey" + items = ([{"severity": "red", "text": t} for t in r.get("red_reasons") or []] + + [{"severity": "yellow", "text": t} for t in r.get("yellow_reasons") or []] + + [{"severity": "info", "text": t} for t in r.get("stale_reasons") or []]) + headline = items[0]["text"] if items else ( + "all checks green" if status == "green" else f"verdict {verdict or 'unknown'}") + return {"status": status, "headline": headline, "updated": r.get("ts"), + "items": items} + + +def read_local(timeout=FALLBACK_TIMEOUT): + """(state, why) from the local Heart CLI's readiness verdict (no tick).""" + cli = resolve_heart_cli() + if cli is None: + return None, "no pyauto-heart on PATH or in the sibling checkout" + try: + proc = subprocess.run([cli, "readiness", "--json"], capture_output=True, + text=True, timeout=timeout) + return from_readiness(json.loads(proc.stdout)), None + except subprocess.TimeoutExpired: + return None, f"pyauto-heart readiness timed out after {timeout}s" + except (OSError, ValueError, AttributeError) as exc: + return None, f"pyauto-heart readiness unreadable ({str(exc)[:80]})" + + +# ----------------------------------------------------------------- render --- + + +def age_of(updated, now=None): + """'2h ago' for an ISO timestamp; None when it cannot be parsed.""" + if not isinstance(updated, str) or not updated: + return None + try: + then = datetime.fromisoformat(updated.replace("Z", "+00:00")) + except ValueError: + return None + if then.tzinfo is None: + then = then.replace(tzinfo=timezone.utc) + secs = max(0, int(((now or datetime.now(timezone.utc)) - then).total_seconds())) + if secs < 60: + return "just now" + for unit, size in (("d", 86400), ("h", 3600), ("m", 60)): + if secs >= size: + return f"{secs // size}{unit} ago" + return "just now" + + +def door_items(state): + """The rows printed under the headline: red/yellow always; info only when + the status is not green and nothing louder exists (a STALE door).""" + items = [i for i in state.get("items") or [] if isinstance(i, dict)] + loud = [i for i in items if i.get("severity") in ("red", "yellow")] + if not loud and state.get("status") != "green": + loud = [i for i in items if i.get("severity") == "info"] + return sorted(loud, key=lambda i: SEVERITY_ORDER.get(i.get("severity"), 3)) + + +def summarise(state, source, url=None, fallback_reason=None): + status = state.get("status") if state else "grey" + return { + "status": status, + "exit": EXIT.get(status, 3), + "headline": (state or {}).get("headline") or "", + "updated": (state or {}).get("updated"), + "age": age_of((state or {}).get("updated")), + "items": [{"severity": i.get("severity"), "text": i.get("text", ""), + "url": i.get("url")} for i in door_items(state or {})], + "source": source, + "url": url, + "fallback_reason": fallback_reason, + } + + +def render(s): + if s["status"] == "grey" and s["source"] == "none": + lines = [f"Heart: GREY — unreachable ({s['fallback_reason']})"] + else: + age = f" (updated {s['age']})" if s["age"] else "" + lines = [f"Heart: {s['status'].upper()} — {s['headline']}{age}"] + for i in s["items"]: + line = f" [{i['severity']}] {i['text']}" + if i.get("url"): + line += f" — {i['url']}" + lines.append(line) + if s["source"] == "local-readiness": + lines.append(f" (from local `pyauto-heart readiness`; published feed: " + f"{s['fallback_reason']})") + return "\n".join(lines) + + +def read(url=None, offline=False): + """The door's summary — published feed first, local readiness second.""" + why = "--offline" + if not offline: + if url is None: + url, why = feed_url() + if url is not None: + feed, why = fetch_feed(url) + if feed is not None: + return summarise(feed, "published", url) + local, local_why = read_local() + if local is not None: + return summarise(local, "local-readiness", url, why) + return summarise(None, "none", url, f"{why}; {local_why}") + + +def main(argv=None): + p = argparse.ArgumentParser( + prog="heart_feed.py", + description="Read the Heart's published state.json (read-only; never ticks).") + p.add_argument("--json", action="store_true", help="machine output") + p.add_argument("--url", help="read this state.json URL instead (tests, mirrors)") + p.add_argument("--offline", action="store_true", + help="skip the published feed; ask local pyauto-heart readiness") + args = p.parse_args(argv) + s = read(url=args.url, offline=args.offline) + print(json.dumps(s, indent=2) if args.json else render(s)) + return s["exit"] + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/WORKFLOW.md b/skills/WORKFLOW.md index 5a41f08..fc49b43 100644 --- a/skills/WORKFLOW.md +++ b/skills/WORKFLOW.md @@ -126,6 +126,12 @@ issue or lifecycle mechanics in Brain. ## Heart gate +**At the door:** development entry (`start_dev` step 0a "Heart at the door", +mirrored by `start_bundle` and `route`) first reads the Heart's published feed +with the read-only `bin/heart_feed.py` — RED stops before planning, YELLOW/STALE +warn and continue. It is a light early read, never a tick; the ship-time gate +below is unchanged and still authoritative. + Before `ship_*`, use the vitals faculty for the authoritative verdict: - GREEN: continue. diff --git a/skills/route/route.md b/skills/route/route.md index c17823e..644bfa9 100644 --- a/skills/route/route.md +++ b/skills/route/route.md @@ -7,6 +7,15 @@ typed shortcuts into the same routing. Shared routing context: `PyAutoBrain/skills/COMMANDS.md`. +## Heart at the door + +When the inferred route enters development (`/feature`, `/bug`, `/refactor`, +`/workspace`, `/docs`, `/research` heading into `/start_dev`), run +`bin/heart_feed.py` **before dispatch** and act on it exactly as +[`start_dev`](../start_dev/start_dev.md) step 0a "Heart at the door" says — a +RED stops before any planning. Read-only routes (`/health`, status questions) +skip it; `/health` is where a RED is fixed. + ## Classify → dispatch Infer the work-type from the request using the taxonomy in diff --git a/skills/start_bundle/start_bundle.md b/skills/start_bundle/start_bundle.md index 03e67eb..8b7b63c 100644 --- a/skills/start_bundle/start_bundle.md +++ b/skills/start_bundle/start_bundle.md @@ -19,6 +19,12 @@ The bundle is an orchestration convenience, never a batching of the record. ## Steps +**Heart at the door — once, before step 1.** Run `bin/heart_feed.py` and act +on it exactly as [`start_dev`](../start_dev/start_dev.md) step 0a "Heart at the +door" says (GREEN/YELLOW/STALE/GREY continue, RED stops the whole bundle before +any member is planned). Once per bundle is enough; the per-member `/start_dev` +runs need not repeat it within the same sitting. + ### 1. Read every member first Read each member prompt in full *before* planning any of them. Confirm they are diff --git a/skills/start_dev/start_dev.md b/skills/start_dev/start_dev.md index aeb8f5a..d0a95fb 100644 --- a/skills/start_dev/start_dev.md +++ b/skills/start_dev/start_dev.md @@ -75,6 +75,25 @@ section required by the current step; a prior accepted plan remains approved. ## Flow +### 0a. Heart at the door + +Before anything else — before the resume check, before planning — run the +read-only helper (never a tick) and show its first line to the user: + +```bash +python3 "${PYAUTO_BRAIN:-$(test -d organs/PyAutoBrain && echo organs/PyAutoBrain || echo PyAutoBrain)}/bin/heart_feed.py" +``` + +- **GREEN** (exit 0) → continue. +- **YELLOW / STALE** (exit 1) → print the reasons and continue; the YELLOW + acknowledgement stays a ship-time act (`ship_*` step 3), unchanged. +- **RED** (exit 2) → **stop before planning**; quote every reason verbatim. + The human may invoke [`../../AUTONOMY.md`](../../AUTONOMY.md) + "Human override for Heart RED (development only)" to proceed — record it on + the issue when invoked. +- **GREY / unreachable** (exit 3) → say so and continue. +- **`--auto` + RED** → park the run before any issue is opened. + ### 0. Sync + resume check (Mind) ```bash diff --git a/tests/test_heart_feed.py b/tests/test_heart_feed.py new file mode 100644 index 0000000..945222f --- /dev/null +++ b/tests/test_heart_feed.py @@ -0,0 +1,196 @@ +"""bin/heart_feed.py — the Heart at the door (PyAutoBrain#423). + +Fixture feeds only: every published read is a `file://` URL in tmp_path, and +the local fallback is a fake `pyauto-heart` placed first on PATH. Nothing here +reaches the network or a real Heart, and no test runs a tick. +""" + +import json +import os +import subprocess +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path + +import pytest + +BRAIN_HOME = Path(__file__).resolve().parents[1] +SCRIPT = BRAIN_HOME / "bin" / "heart_feed.py" +sys.path.insert(0, str(BRAIN_HOME / "bin")) + +import heart_feed # noqa: E402 + + +def _iso(hours_ago=2): + t = datetime.now(timezone.utc) - timedelta(hours=hours_ago, minutes=1) + return t.strftime("%Y-%m-%dT%H:%M:%SZ") + + +def _feed(status, headline, items): + return {"schema_version": 1, "organ": "heart", "repo": "HeartRepo", + "status": status, "headline": headline, "updated": _iso(), + "pages_url": "https://example.invalid/HeartRepo/", "items": items} + + +FEEDS = { + "green": _feed("green", "all checks green", []), + "yellow": _feed("yellow", "2 yellow reasons", [ + {"severity": "yellow", "text": "manifest drift: 3 mismatches"}, + {"severity": "info", "text": "install verification not run"}, + ]), + "red": _feed("red", "workspace smoke failing", [ + {"severity": "info", "text": "install verification not run"}, + {"severity": "red", "text": "smoke: example_script.py failed", + "url": "https://example.invalid/run/1"}, + {"severity": "yellow", "text": "manifest drift: 1 mismatch"}, + ]), + "stale": _feed("stale", "no release validation for current source", [ + {"severity": "info", "text": "no release validation for current source"}, + ]), + "grey": _feed("grey", "heart has never ticked", []), +} + + +def _write(tmp_path, name, payload): + path = tmp_path / f"{name}.json" + path.write_text(payload if isinstance(payload, str) else json.dumps(payload)) + return path.as_uri() + + +@pytest.fixture +def no_local_heart(tmp_path, monkeypatch): + """PATH without pyauto-heart and no sibling checkout to fall back on.""" + monkeypatch.setattr(heart_feed, "resolve_heart_cli", lambda: None) + + +@pytest.fixture +def fake_heart(tmp_path, monkeypatch): + """A fake `pyauto-heart` first on PATH answering `readiness --json`.""" + bindir = tmp_path / "fakebin" + bindir.mkdir() + log = tmp_path / "calls.log" + + def install(readiness): + cli = bindir / "pyauto-heart" + cli.write_text( + "#!/usr/bin/env bash\n" + f"echo \"$@\" >> {log}\n" + "if [ \"$1\" = readiness ]; then cat <<'EOF'\n" + f"{json.dumps(readiness)}\nEOF\nfi\n") + cli.chmod(0o755) + monkeypatch.setenv("PATH", f"{bindir}{os.pathsep}{os.environ['PATH']}") + return log + + return install + + +@pytest.mark.parametrize("status,code", [ + ("green", 0), ("yellow", 1), ("stale", 1), ("red", 2), ("grey", 3)]) +def test_exit_code_per_status(tmp_path, no_local_heart, status, code): + url = _write(tmp_path, status, FEEDS[status]) + assert heart_feed.main(["--url", url]) == code + + +def test_red_prints_headline_then_reasons_worst_first(tmp_path, no_local_heart, capsys): + url = _write(tmp_path, "red", FEEDS["red"]) + assert heart_feed.main(["--url", url]) == 2 + lines = capsys.readouterr().out.splitlines() + assert lines[0] == "Heart: RED — workspace smoke failing (updated 2h ago)" + assert lines[1] == (" [red] smoke: example_script.py failed — " + "https://example.invalid/run/1") + assert lines[2] == " [yellow] manifest drift: 1 mismatch" + # info rows are not printed when louder rows exist + assert not any("install verification" in ln for ln in lines) + + +def test_stale_prints_its_info_rows(tmp_path, no_local_heart, capsys): + url = _write(tmp_path, "stale", FEEDS["stale"]) + heart_feed.main(["--url", url]) + out = capsys.readouterr().out.splitlines() + assert out[0].startswith("Heart: STALE — no release validation") + assert out[1] == " [info] no release validation for current source" + + +def test_green_prints_one_line(tmp_path, no_local_heart, capsys): + url = _write(tmp_path, "green", FEEDS["green"]) + heart_feed.main(["--url", url]) + assert capsys.readouterr().out.splitlines() == [ + "Heart: GREEN — all checks green (updated 2h ago)"] + + +def test_json_output_is_the_summary(tmp_path, no_local_heart, capsys): + url = _write(tmp_path, "red", FEEDS["red"]) + assert heart_feed.main(["--url", url, "--json"]) == 2 + s = json.loads(capsys.readouterr().out) + assert s["status"] == "red" and s["exit"] == 2 and s["source"] == "published" + assert [i["severity"] for i in s["items"]] == ["red", "yellow"] + assert s["url"] == url + + +@pytest.mark.parametrize("payload", [ + "{not json", json.dumps([1, 2]), json.dumps({"status": "purple", "items": []}), + json.dumps({"status": "red", "items": "nope"})]) +def test_malformed_feed_without_fallback_is_grey(tmp_path, no_local_heart, capsys, payload): + url = _write(tmp_path, "bad", payload) + assert heart_feed.main(["--url", url]) == 3 + assert capsys.readouterr().out.startswith("Heart: GREY — unreachable (") + + +def test_unreachable_feed_without_fallback_is_grey(tmp_path, no_local_heart, capsys): + url = (tmp_path / "missing.json").as_uri() + assert heart_feed.main(["--url", url]) == 3 + out = capsys.readouterr().out + assert out.startswith("Heart: GREY — unreachable (") + assert "no pyauto-heart" in out # the reason names both legs that failed + + +def test_unreachable_feed_falls_back_to_local_readiness(tmp_path, fake_heart, capsys): + log = fake_heart({"verdict": "red", "red_reasons": ["smoke failing: a.py"], + "yellow_reasons": ["drift"], "stale_reasons": ["old"], + "ts": _iso(3)}) + url = (tmp_path / "missing.json").as_uri() + assert heart_feed.main(["--url", url]) == 2 + lines = capsys.readouterr().out.splitlines() + assert lines[0] == "Heart: RED — smoke failing: a.py (updated 3h ago)" + assert lines[1] == " [red] smoke failing: a.py" + assert lines[2] == " [yellow] drift" + assert "local `pyauto-heart readiness`" in lines[-1] + # read-only: the fallback asks for the verdict, never a tick + assert log.read_text().split("\n")[0] == "readiness --json" + assert "tick" not in log.read_text() + + +def test_offline_skips_the_feed(tmp_path, fake_heart, monkeypatch, capsys): + fake_heart({"verdict": "green", "red_reasons": [], "yellow_reasons": [], + "stale_reasons": [], "ts": _iso(1)}) + monkeypatch.setattr(heart_feed, "fetch_feed", + lambda *a, **k: pytest.fail("--offline fetched the feed")) + assert heart_feed.main(["--offline"]) == 0 + assert capsys.readouterr().out.startswith( + "Heart: GREEN — all checks green (updated 1h ago)") + + +def test_readiness_mapping_shape(): + s = heart_feed.from_readiness({"verdict": "stale", "stale_reasons": ["x"], + "ts": "2026-01-01T00:00:00+00:00"}) + assert s == {"status": "stale", "headline": "x", + "updated": "2026-01-01T00:00:00+00:00", + "items": [{"severity": "info", "text": "x"}]} + assert heart_feed.from_readiness({"verdict": "??"})["status"] == "grey" + + +def test_url_derives_from_policy_and_pages_base(monkeypatch): + monkeypatch.setenv("BOARD_PAGES_BASE", "https://pages.example.invalid/") + url, why = heart_feed.feed_url() + assert why is None + assert url == (f"https://pages.example.invalid/" + f"{heart_feed.heart_board_repo()}/state.json") + + +def test_cli_exit_code_end_to_end(tmp_path): + url = _write(tmp_path, "red", FEEDS["red"]) + env = {**os.environ, "PATH": "/usr/bin:/bin"} + r = subprocess.run([sys.executable, str(SCRIPT), "--url", url], + capture_output=True, text=True, env=env, timeout=60) + assert r.returncode == 2 + assert r.stdout.splitlines()[0].startswith("Heart: RED — workspace smoke failing") diff --git a/tests/test_skill_seams.py b/tests/test_skill_seams.py new file mode 100644 index 0000000..a5ad93e --- /dev/null +++ b/tests/test_skill_seams.py @@ -0,0 +1,43 @@ +"""Skill seams for the Heart at the door (PyAutoBrain#423). + +The development entry skills must name the step, and start_dev must point at +the AUTONOMY.md override by its section title rather than restating the rule — +so a rename of either side fails here instead of leaving a dangling pointer. +""" + +from pathlib import Path + +BRAIN_HOME = Path(__file__).resolve().parents[1] +SKILLS = BRAIN_HOME / "skills" +STEP = "Heart at the door" +OVERRIDE = "Human override for Heart RED (development only)" + + +def _read(rel): + return (SKILLS / rel).read_text(encoding="utf-8") + + +def test_entry_skills_name_the_step(): + for rel in ("start_dev/start_dev.md", "start_bundle/start_bundle.md", + "route/route.md"): + assert STEP in _read(rel), rel + + +def test_start_dev_step_runs_the_helper_before_the_resume_check(): + text = _read("start_dev/start_dev.md") + door, resume = text.index(f"### 0a. {STEP}"), text.index("### 0. Sync + resume") + assert door < resume + assert "bin/heart_feed.py" in text[door:resume] + assert (BRAIN_HOME / "bin" / "heart_feed.py").is_file() + + +def test_start_dev_points_at_the_autonomy_override_by_title(): + assert OVERRIDE in _read("start_dev/start_dev.md") + assert f"## {OVERRIDE}" in (BRAIN_HOME / "AUTONOMY.md").read_text(encoding="utf-8") + + +def test_workflow_heart_gate_mentions_the_door(): + text = _read("WORKFLOW.md") + section = text[text.index("## Heart gate"):] + section = section[:section.index("\n## ", 1)] + assert STEP in section and "heart_feed.py" in section