From ff9a2765b4b456b5519ebab5011bb44f415c7bfb Mon Sep 17 00:00:00 2001
From: Jammy2211
Date: Thu, 3 Sep 2026 10:31:50 -0400
Subject: [PATCH 1/2] feat(batch): batch status box on both dashboards +
rolling science review (#341)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Both dashboards open with one box: the open slot, every member on one line
as in progress / awaiting review / ruled, and the button to the Pages packet
once there is something to review. The reading lives in one stdlib-only
module (`agents/conductors/batch/_status.py`) so the Cortex's bare Pages
job and the Mind's dashboard render it byte-consistently; nothing in the
box is time-derived, so `--check` stays deterministic.
Mind (dev) rule unchanged: dispatched at once, reviewed at once, the button
appears when `collected:` is stamped.
Cortex (science) rule is rolling: the button appears as soon as one member
is awaiting-ruling; submitted/running members show as in progress with the
record's own wall/budget/refresh note and hold no review control. A slot
stays open across partial reviews — later sittings land as
`batches/reviews/-r.md`, `collect --kind cortex` merges them
(ruled sticky), keeps refreshing the packet while anything is on the board,
never re-splices a ruled member, and closes the slot only when nothing is
left in a board state (carried members excluded). The dev refusal on an
existing review file stays.
- `_status.py`: the vocabulary (`RULING_WORDS`, `PENDING_RE`,
`CORTEX_BOARD_STATES`, `RECORD_KEY`), the lossless `read_record`,
`dev_status` / `cortex_status`, `pick_slot` (latest `dispatched:`, never
lexical), `render_html` / `render_md`, fixture "No batch in flight."
- `_batch.py`: imports the vocabulary back; partial-review merge, next-free
review path in `organ_for`, `record_update_cortex` closes on the board
and repeats `- review:` per sitting; `_apply_packet` refuses only for dev.
- `_cortex.py` / `_intake.py`: the census `batch` reading and the box above
"awaiting" / "Start here" in both twins; `_theme.py` gains `a.go`.
- Doctrine in `agents/conductors/batch/AGENTS.md`.
- Tests: new `test_batch_status.py`; rolling-slot cases in
`test_batch_kinds.py`; box tests in `test_cortex_conductor.py` and
`test_intake_dashboard.py`. 861 pass.
Pairs with PyAutoLabs/PyAutoCortex#7 (merge first) and the PyAutoMind
`batches/**` trigger PR (merge last). Cites PyAutoBrain#341.
Co-Authored-By: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_01SgtS7NhqeziqgxKfSiMzRL
---
agents/conductors/batch/AGENTS.md | 66 +++++-
agents/conductors/batch/_batch.py | 164 ++++++++++++---
agents/conductors/batch/_status.py | 301 ++++++++++++++++++++++++++++
agents/conductors/cortex/_cortex.py | 70 ++++---
agents/conductors/intake/_intake.py | 67 +++++++
board/_theme.py | 5 +
tests/test_batch_kinds.py | 188 ++++++++++++++++-
tests/test_batch_status.py | 193 ++++++++++++++++++
tests/test_cortex_conductor.py | 49 ++++-
tests/test_intake_dashboard.py | 85 +++++++-
10 files changed, 1109 insertions(+), 79 deletions(-)
create mode 100644 agents/conductors/batch/_status.py
create mode 100644 tests/test_batch_status.py
diff --git a/agents/conductors/batch/AGENTS.md b/agents/conductors/batch/AGENTS.md
index 956c18f9..619c9331 100644
--- a/agents/conductors/batch/AGENTS.md
+++ b/agents/conductors/batch/AGENTS.md
@@ -472,14 +472,34 @@ lost; the `notes: |` block is never touched.
### The close leg
-A review that has landed closes the batch. Where `batches/reviews/.md`
-exists, collect **refuses to write the packet** — that file is the record now,
-and rewriting the page the human ruled on destroys what they ruled against —
-and instead fills `reviewed-at:`, `review-minutes-actual:` and `review:` from
-it — **fills, never overwrites**: a value the record already carries (anything
-but blank or `(not given)`) is the human's and stays. The decisions inside are
-reported, never enacted: merges, follow-ups and
-rejections are the orchestrator's next act with the human, not collect's.
+**`dev` — collected once, reviewed once.** A review that has landed closes the
+batch outright. Where `batches/reviews/.md` exists, collect **refuses to
+write the packet** — that file is the record now, and rewriting the page the
+human ruled on destroys what they ruled against — and instead fills
+`reviewed-at:`, `review-minutes-actual:` and `review:` from it — **fills,
+never overwrites**: a value the record already carries (anything but blank or
+`(not given)`) is the human's and stays. The decisions inside are reported,
+never enacted: merges, follow-ups and rejections are the orchestrator's next
+act with the human, not collect's. The review button lives on `collected:` —
+there is exactly one packet to press it on, because a dev batch is dispatched
+at once and reviewed at once.
+
+**`cortex` — a rolling board, reviewed as many times as it takes.** A review
+landing does **not** by itself refuse the packet write: `PyAutoCortex/batches/
+AGENTS.md`, "The first review does not close the slot" — the human rules on
+whatever is reviewable whenever they come in, and the packet keeps refreshing
+under whatever is still on the board while they do. A later sitting lands as
+`batches/reviews/-r.md` (N ≥ 2; the first sitting is `.md` — see
+"Two kinds, two records" below), and each one adds its own `- review:` line to
+the record rather than replacing the last. What a review DOES do, on every
+kind, is take its own members out of further splicing (`_on_the_board`): a
+member the record already shows `ruled: yes` for is never re-rendered, on this
+sitting's collect or any later one — its packet span is what the human was
+shown. The record's `delivered:`/`packet:` keys are filled once the slot is
+CLOSED — nothing left in a board state, carried members excluded — and moved
+on every refresh until then; `reviewed-at:`/`review-minutes-actual:` still
+fill from whichever sitting sets them first, and `carried:` is written once
+per member the moment a sitting finds it still running.
### The extension point
@@ -521,6 +541,36 @@ with their job ids and wall-vs-budget, and **hold no review control at all**:
no chips, no ruled box, `(none)` in the submitted review. There is nothing for
the human to say about a run that has not finished.
+**The first review does not close the slot, either.** A dev batch is reviewed
+once — `collected:` puts the button on the packet, and `batches/reviews/
+.md` existing closes it outright (see "The close leg" above). A Cortex
+slot stays open across as many sittings as it takes: the human rules on
+whatever is reviewable whenever they come in, and every sitting after the
+first lands as its own numbered file, `batches/reviews/-r.md` (N ≥
+2 — `-r1` is not a name, the first sitting is `.md`;
+`PyAutoCortex/batches/reviews/AGENTS.md`). `organ_for`'s `review_path` always
+names the **next free** one, so the packet's submit button (and the GitHub
+new-file link it builds) never points a later sitting at an earlier one's
+file. The record's `- review:` key **repeats**, one line per sitting, in the
+order they happened — `collect` appends whichever of a slot's review files the
+record does not already carry a line for, and never touches the ones it does.
+
+**A rolling slot closes when nothing is left in a board state.** Not "a review
+exists" — a Cortex record may carry several `- review:` lines and still be
+open, because a later sitting can rule on members a pull only just landed.
+Closed means: every member is either **ruled** (its review row says `ruled:
+yes` — `_on_the_board` drops it from the score for good, so no later sitting
+re-splices it even if the underlying phase file changes afterward) or
+**carried** (a `- carried: — still at review` line for it is
+already IN THE RECORD, written by an earlier sitting). A member that is merely
+`submitted`/`running`/`pulled`/`awaiting-ruling` **right now**, with no
+`- carried:` line yet, still holds the slot open — carrying happens at a
+sitting, not by inference from the tree. This is the same reading
+`agents/conductors/batch/_status.py`'s `cortex_status` gives the status box on
+both dashboards (`_cortex_slot_open` in `_batch.py` is the collect-side
+replay of it), so a slot the box shows as still in flight is a slot `collect`
+also still treats as open.
+
**Carry-forward is the mechanism that moves them on.** At close, every member
still `submitted`/`running` gets a `- carried: — still at review`
line on the record, and the next `plan --kind cortex --apply` includes those
diff --git a/agents/conductors/batch/_batch.py b/agents/conductors/batch/_batch.py
index bf14f0e9..08375897 100644
--- a/agents/conductors/batch/_batch.py
+++ b/agents/conductors/batch/_batch.py
@@ -47,6 +47,14 @@
effective_difficulty, effective_review_minutes, effective_unattended,
parse_prompt, priority_rank,
)
+# `_status` (same directory) is the one definition of this vocabulary — the
+# batch status box on both dashboards reads a record the same way this
+# conductor writes one. Re-exported below (`RULING_WORDS`, `PENDING_RE`,
+# `CORTEX_BOARD_STATES`) so existing importers of `_batch` keep working.
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from _status import ( # noqa: E402
+ PENDING_RE, RULING_WORDS, CORTEX_BOARD_STATES, _carried_slugs,
+)
# One slot's worth of the human's attention, and only the DEFAULT: the human
# sets it per slot with --budget, because they know whether the next one is a
@@ -340,21 +348,10 @@ def emit(d: dict) -> None:
RECORD_KEY = re.compile(r"^- ([a-z][a-z0-9-]*):(?:\s+(.*?))?\s*$")
MEMBER_RE = re.compile(r"^ - (?P[^:]+): (?P.+)$")
-#: Outcome tokens a human (or an earlier collect) already ruled with. A record
-#: line opening with one of these is a VERDICT, and `--apply` leaves it alone:
-#: the am record's REJECTED/ACCEPTED members are the ledger of a review that
-#: happened, and overwriting them with a fresh machine reading would erase it.
-RULING_WORDS = {"DELIVERED", "NOT-DELIVERED", "FAILED", "MERGED", "PARKED",
- "CARRIED", "SUSPECT", "REJECTED", "ACCEPTED", "UNREVIEWED",
- "STRUCTURE-OK", "LEAVE-TO-FINISH", "TWEAK", "DEFER", "PENDING",
- "HEALTHY", "MERGE", "REJECT", "DROP", "DROPPED", "RERUN",
- "ACCEPT"}
-
-#: An outcome that says the member had not finished when the record was
-#: written. These are the words the ledger actually uses (`REFRESHED … still
-#: carried`, `CARRIED to the next batch`), not a vocabulary invented here.
-PENDING_RE = re.compile(r"\b(RUNNING|CARRIED|IN FLIGHT|REFRESHED|PENDING|"
- r"DISPATCHED)\b", re.I)
+# `RULING_WORDS` and `PENDING_RE` are `_status`'s (imported above): a record
+# line opening with a ruling word is a VERDICT, and `--apply` leaves it alone;
+# `PENDING_RE` is the ledger's own "not finished yet" vocabulary. Kept as one
+# definition so the batch status box and this conductor never drift.
PR_URL_RE = re.compile(r"https://github\.com/([\w.-]+/[\w.-]+)/pull/(\d+)")
ISSUE_URL_RE = re.compile(r"https://github\.com/[\w.-]+/[\w.-]+/issues/\d+")
@@ -1210,10 +1207,14 @@ def collect(mind: Path, slot: str, *, evidence: dict | None = None,
"evidence": evidence or {}, "notes": notes, "kinds": kinds,
"cortex": cortex}
- review_path = root / "batches" / "reviews" / f"{slot}.md"
- review = (read_review(review_path.read_text(encoding="utf-8",
- errors="replace"))
- if review_path.is_file() else None)
+ if kind == "cortex":
+ # A rolling slot may have more than one sitting — see `_merge_reviews`.
+ review = _merge_reviews(_review_paths(root, slot))
+ else:
+ review_path = root / "batches" / "reviews" / f"{slot}.md"
+ review = (read_review(review_path.read_text(encoding="utf-8",
+ errors="replace"))
+ if review_path.is_file() else None)
ruled = {slug for slug, row in (review or {}).get("members", {}).items()
if row.get("ruled") == "yes"}
@@ -2145,10 +2146,15 @@ def _apply_packet(root: Path, d: dict) -> list:
page is the same page. Written under the organ's own `batches/packets/`."""
notes: list[str] = []
packet = root / "batches" / "packets" / f"{d['slot']}.html"
- if d.get("review") is not None:
+ if d.get("kind") != "cortex" and d.get("review") is not None:
# `batches/packets/AGENTS.md`: the archived page and the review file are
# the audit pair — what was shown, what was ruled. A correction gets a
- # new dated page, not an edit.
+ # new dated page, not an edit. A dev batch is reviewed once — this is
+ # the WHOLE close. A Cortex slot is a rolling board (`batches/AGENTS.md`,
+ # "The first review does not close the slot"): a review lands, more
+ # members may still join the board, and the packet keeps refreshing
+ # under `d["members"]` (which `_on_the_board` already keeps the ruled
+ # ones out of) until nothing is left on it.
notes.append("review submitted — the packet is never rewritten after a "
f"review; batches/reviews/{d['slot']}.md is the record now")
return notes
@@ -2251,10 +2257,10 @@ def read_review(text: str) -> dict:
CORTEX_CONDUCTOR = BRAIN / "agents" / "conductors" / "cortex" / "_cortex.py"
_CX = None
-#: The four states a science member is on the board in. `ready` and `planned`
-#: are scope; `accepted`, `rerun` and `dropped` are history the rulings ledger
-#: carries. A record naming one of those is reported, never scored.
-CORTEX_BOARD_STATES = ("submitted", "running", "pulled", "awaiting-ruling")
+# `CORTEX_BOARD_STATES` is `_status`'s (imported above) — the four states a
+# science member is on the board in. `ready` and `planned` are scope;
+# `accepted`, `rerun` and `dropped` are history the rulings ledger carries. A
+# record naming one of those is reported, never scored.
#: The Cortex's review vocabulary (`scripts/cortex.py::RULING_VERBS`), not the
#: Mind's merge/tweak/reject/defer. A RUNNING member gets none of them.
@@ -2268,6 +2274,75 @@ def read_review(text: str) -> dict:
"not a resume. Members still running hold no review control; "
"carry-forward moves them to the next board.")
+#: The `-r` suffix of a slot's later, partial reviews (`-r2.md`,
+#: N >= 2) — `scripts/cortex.py`'s own pattern, kept byte-identical so a
+#: review file this conductor offers is one `cortex.py check` also resolves.
+REVIEW_PART_RE = re.compile(r"-r(\d+)$")
+
+
+def _review_paths(root: Path, slot: str) -> list:
+ """Every partial review file a rolling slot has, in sitting order:
+ `reviews/.md` (the first sitting, if it exists) then `-r2.md`,
+ `-r3.md`, … by N — `scripts/cortex.py::batch_problems`'s own resolution.
+ `-r1` is not a name; the first review is `.md`."""
+ reviews = root / "batches" / "reviews"
+ out = []
+ first = reviews / f"{slot}.md"
+ if first.is_file():
+ out.append(first)
+ numbered = []
+ for p in reviews.glob(f"{slot}-r*.md"):
+ m = REVIEW_PART_RE.search(p.stem)
+ if m:
+ numbered.append((int(m.group(1)), p))
+ numbered.sort(key=lambda t: t[0])
+ return out + [p for _, p in numbered]
+
+
+def _next_review_path(root: Path, slot: str) -> str:
+ """The next free `reviews/[-r].md` — `.md` if the first
+ sitting has not happened yet, else the smallest free `-r`, N >= 2."""
+ reviews = root / "batches" / "reviews"
+ if not (reviews / f"{slot}.md").is_file():
+ return f"batches/reviews/{slot}.md"
+ n = 2
+ while (reviews / f"{slot}-r{n}.md").is_file():
+ n += 1
+ return f"batches/reviews/{slot}-r{n}.md"
+
+
+def _merge_reviews(paths: list) -> dict | None:
+ """Every partial review file for a slot, read and merged — `keys` from
+ the latest file that set them, `members` per slug from the latest file
+ that named it. `ruled` is STICKY: a member already ruled `yes` at an
+ earlier sitting stays ruled even if a later, narrower sitting (one that
+ lists only what it itself rules on) does not mention it again — that is
+ what lets `_on_the_board` keep skipping it forever, not just this pass.
+ """
+ if not paths:
+ return None
+ keys: dict = {}
+ members: dict = {}
+ for p in paths:
+ r = read_review(p.read_text(encoding="utf-8", errors="replace"))
+ keys.update(r["keys"])
+ for slug, row in r["members"].items():
+ if members.get(slug, {}).get("ruled") == "yes":
+ continue
+ members[slug] = row
+ return {"keys": keys, "members": members}
+
+
+def _review_rel_paths(d: dict) -> list:
+ """`batches/reviews/[-r].md`, one per file this slot has ever
+ had, in sitting order. Recomputed from the tree rather than threaded
+ through `d`: the review files on disk do not move between a collect and
+ its own apply."""
+ root = d.get("root")
+ if root is None or not d.get("slot"):
+ return []
+ return [f"batches/reviews/{p.name}" for p in _review_paths(root, d["slot"])]
+
class CortexMissing(BatchUsageError):
"""No usable Cortex tree. Its own words, and its own exit code — a session
@@ -2321,7 +2396,9 @@ def organ_for(kind: str, root: Path, slot: str) -> dict:
return {
"key": "cortex", "repo": cx.CORTEX_REPO, "home": cx._home(root),
"packet_path": f"{cx.CORTEX_REPO}/batches/packets/{slot}.html",
- "review_path": f"batches/reviews/{slot}.md",
+ # The rolling board's first review is `.md`; every later
+ # sitting is the next free `-r.md` (`batches/reviews/AGENTS.md`).
+ "review_path": _next_review_path(root, slot),
# `cortex.py check` requires every `##` heading in a review to name
# a member of the record, so the follow-ups block is one level down.
"followups_heading": "### Follow-ups accepted",
@@ -2722,6 +2799,27 @@ def _cortex_movers(d: dict) -> list:
return out
+def _recorded_carried_slugs(rec: dict) -> set:
+ """Slugs an EARLIER sitting already carried forward — `_status`'s own
+ reading of the `- carried:` line, replayed against this record's
+ already-read `rec` so the box and the close leg can never disagree."""
+ return _carried_slugs(rec["keys"], rec["members"])
+
+
+def _cortex_slot_open(d: dict, rec: dict) -> bool:
+ """Whether the science board still holds something worth waiting for —
+ `_status.cortex_status`'s reading: nothing left in a board state, members
+ an EARLIER sitting already carried forward excluded. `d["members"]` is
+ already the board (`_on_the_board` has dropped the ruled and the
+ off-board); a member leaves it for `carried` only once a `- carried:` line
+ naming it is already IN THE RECORD — one merely running right now, before
+ any sitting has said so, still holds this slot open (`batches/AGENTS.md`:
+ "the slot stays open while any non-carried member is still `submitted`,
+ `running`, `pulled` or `awaiting-ruling`")."""
+ carried = _recorded_carried_slugs(rec)
+ return any(s["slug"] not in carried for s in d.get("members") or [])
+
+
def record_update_cortex(text: str, d: dict, stamp: str,
review_at: str = "") -> str:
"""The Cortex record's collect keys, updated in place. The MEMBER LINES ARE
@@ -2729,7 +2827,11 @@ def record_update_cortex(text: str, d: dict, stamp: str,
the dev rewrite (`HEALTHY (…)`) would fail `cortex.py check` outright."""
lines = text.split("\n")
rec = read_record(text)
- closed = d.get("review") is not None
+ # A slot closes when nothing is left on the board (`batches/AGENTS.md`,
+ # "The first review does not close the slot") — NOT the moment a review
+ # file exists: a rolling slot may be reviewed more than once while it is
+ # still open.
+ closed = not _cortex_slot_open(d, rec)
inserts: list[str] = []
if not rec["key_lines"].get("collected"):
inserts.append(f"- collected: {stamp}")
@@ -2744,8 +2846,12 @@ def record_update_cortex(text: str, d: dict, stamp: str,
_set_key(lines, rec, "review-at", review_at.strip(), inserts)
review = d.get("review")
if review is not None:
- _fill_key(lines, rec, "review",
- f"batches/reviews/{d['slot']}.md", inserts)
+ # `review:` REPEATS — one line per sitting (`batches/reviews/AGENTS.md`)
+ # — so this is never a fill-once key; only lines the record does not
+ # already carry are appended, in sitting order.
+ have = set(rec["keys"].get("review") or [])
+ inserts += [f"- review: {p}" for p in _review_rel_paths(d)
+ if p not in have]
reviewed_at = review["keys"].get("reviewed-at", "")
if reviewed_at:
_fill_key(lines, rec, "reviewed-at", reviewed_at, inserts)
diff --git a/agents/conductors/batch/_status.py b/agents/conductors/batch/_status.py
new file mode 100644
index 00000000..f6162642
--- /dev/null
+++ b/agents/conductors/batch/_status.py
@@ -0,0 +1,301 @@
+#!/usr/bin/env python3
+"""agents/conductors/batch/_status.py — one reading of "is a batch in flight?".
+
+Both dashboards open with the same box: the slot that is in flight, every
+member on one line, and — once there is something to review — the button to
+its packet. The Mind's page and the Cortex's page render it from different
+state, so the *reading* lives here and each organ only supplies its facts.
+
+Three constraints shape this module, and they are why it is a module at all:
+
+- **Stdlib and `_theme` only.** The Cortex renderer runs bare inside
+ PyAutoCortex's own `dashboard_refresh.yml`, with only the Cortex and the
+ Brain checked out and nothing installed. So this module imports neither
+ `_batch` (which imports `_sizing`, which reads the Mind's body map at
+ import) nor the Cortex script. It is imported BY `_batch.py`, never the
+ other way round.
+- **One renderer, two organs.** `render_html`/`render_md` are written once so
+ the two boards' `--check` runs stay byte-consistent with each other and a
+ reader who knows one page can read the other.
+- **Nothing time-derived.** Every word in the box comes from a record line or
+ a phase file. A box that said "3h ago" would be drift on every re-render of
+ an unchanged tree, and `dashboard --check` would report it as such.
+
+The vocabulary (`RECORD_KEY`, `RULING_WORDS`, `PENDING_RE`,
+`CORTEX_BOARD_STATES`) lives here and `_batch.py` imports it back, so the
+words the record is written in have one definition.
+"""
+
+from __future__ import annotations
+
+import html as _html
+import re
+import sys
+from pathlib import Path
+
+BRAIN_HOME = Path(__file__).resolve().parents[3]
+
+# The shared board theme: presentation only, and the one place that answers
+# "what does a one-tap board look like" — same import shape as `_cortex.py`,
+# for the same reason (that page renders with only Brain + Cortex present).
+sys.path.insert(0, str(BRAIN_HOME / "board"))
+from _theme import pills # noqa: E402
+
+# --------------------------------------------------------- the vocabulary ---
+#: The batch record's own grammar. Kept byte-identical to `_batch.py`'s and
+#: `_cortex.py`'s reader so all three organs read one record the same way.
+RECORD_KEY = re.compile(r"^- ([a-z][a-z0-9-]*):(?:\s+(.*?))?\s*$")
+
+#: Outcome tokens a human (or an earlier collect) already ruled with. A record
+#: line opening with one of these is a VERDICT, and `--apply` leaves it alone.
+RULING_WORDS = {"DELIVERED", "NOT-DELIVERED", "FAILED", "MERGED", "PARKED",
+ "CARRIED", "SUSPECT", "REJECTED", "ACCEPTED", "UNREVIEWED",
+ "STRUCTURE-OK", "LEAVE-TO-FINISH", "TWEAK", "DEFER", "PENDING",
+ "HEALTHY", "MERGE", "REJECT", "DROP", "DROPPED", "RERUN",
+ "ACCEPT"}
+
+#: An outcome that says the member had not finished when the record was
+#: written. These are the words the ledger actually uses (`REFRESHED … still
+#: carried`, `CARRIED to the next batch`), not a vocabulary invented here.
+PENDING_RE = re.compile(r"\b(RUNNING|CARRIED|IN FLIGHT|REFRESHED|PENDING|"
+ r"DISPATCHED)\b", re.I)
+
+#: The four states a science member is on the board in. `ready` and `planned`
+#: are scope; `accepted`, `rerun` and `dropped` are history the rulings ledger
+#: carries. A slot whose every non-carried member has left these is closed.
+CORTEX_BOARD_STATES = ("submitted", "running", "pulled", "awaiting-ruling")
+
+#: The four things this box says about a member. They are display words, not
+#: record states: the record's vocabulary differs per kind, the box's does not.
+IN_PROGRESS = "in progress"
+AWAITING = "awaiting review"
+RULED = "ruled"
+PULLED = "pulled, awaiting collect"
+NOT_DELIVERED = "not delivered"
+
+#: Tone per display word, as `_theme.pills` classes: go / caution / neutral.
+_TONE = {IN_PROGRESS: "n", AWAITING: "y", RULED: "g", PULLED: "n",
+ NOT_DELIVERED: "r"}
+
+#: What the box says when nothing is in flight. A fixed sentence, so an
+#: unchanged tree renders an unchanged page.
+FIXTURE = "No batch in flight."
+
+
+def read_record(text: str, member_re=None) -> dict:
+ """One batch record as `{"keys": {k: [v, …]}, "members": [row, …]}`.
+
+ Every value of every key is kept, in file order — the lossless reader the
+ live-progress strip and this box both need (`- refreshed:` repeats once
+ per pull and each one is history). `member_re` is the CALLER's member
+ grammar: the dev and science records spell a member row differently, and
+ neither spelling is invented here.
+ """
+ keys: dict[str, list[str]] = {}
+ members: list[dict] = []
+ in_members = False
+ for lineno, raw in enumerate(text.split("\n"), 1):
+ m = RECORD_KEY.match(raw)
+ if m:
+ in_members = m.group(1) == "members"
+ keys.setdefault(m.group(1), []).append((m.group(2) or "").strip())
+ continue
+ if in_members and raw.startswith(" - "):
+ row = {"lineno": lineno, "raw": raw}
+ mm = member_re.match(raw.replace("--", "—")) if member_re else None
+ if mm:
+ row.update({k: (mm.groupdict().get(k) or "").strip()
+ for k in ("slug", "path", "runs", "minutes", "state")
+ if k in mm.groupdict()})
+ members.append(row)
+ elif raw.strip() and not raw.startswith(" ") and in_members:
+ in_members = False
+ return {"keys": keys, "members": members}
+
+
+# ------------------------------------------------------------- the status ---
+def _one(keys: dict, name: str) -> str:
+ """The first value of a record key, `''` when the key is absent."""
+ values = keys.get(name) or []
+ return (values[0] or "").strip() if values else ""
+
+
+def _head(text: str, limit: int = 72) -> str:
+ """The head of an outcome sentence — one line, elided, never wrapped."""
+ head = " ".join((text or "").split())
+ return head if len(head) <= limit else head[:limit - 1].rstrip() + "…"
+
+
+def _first_word(text: str) -> str:
+ words = (text or "").split()
+ return words[0].strip("(,.").upper() if words else ""
+
+
+def _carried_slugs(keys: dict, members: list) -> set:
+ """The members a `- carried:` line hands to the next slot.
+
+ A carried member is on the NEXT board, not this one, so it must not hold
+ this slot open — `carried: refs_v1_… — still submitted at review` closes a
+ slot whose other three members are ruled. Only the head of the line (what
+ comes before the em-dash that opens the human's reason) names slugs.
+ """
+ slugs = {m.get("slug") for m in members if m.get("slug")}
+ out = set()
+ for value in keys.get("carried") or []:
+ head = re.split(r"\s+[—–-]\s+", value.replace("--", "—"), 1)[0]
+ for token in re.split(r"[\s,;]+", head):
+ token = token.strip("`'\"()")
+ if token in slugs:
+ out.add(token)
+ return out
+
+
+def dev_status(slot: str, keys: dict, members: list, review_exists: bool,
+ pages: str) -> dict | None:
+ """The Mind's (development) reading — today's behaviour, unchanged.
+
+ A dev batch is dispatched at once and reviewed at once: it is open until
+ it has been reviewed, and the review button appears when `collect` has
+ stamped `collected:` — that is when a packet exists to press it on.
+ """
+ if _one(keys, "reviewed-at") or review_exists:
+ return None
+ rows = []
+ for m in members:
+ outcome = (m.get("outcome") or "").strip()
+ if not outcome:
+ state, detail = NOT_DELIVERED, ""
+ elif PENDING_RE.search(outcome):
+ state, detail = IN_PROGRESS, _head(outcome)
+ else:
+ # A ruling word (`RULING_WORDS`) or a collect health reading —
+ # either way the member ended and a human can read it.
+ state, detail = AWAITING, _head(outcome)
+ rows.append({"slug": m.get("slug", ""), "state": state,
+ "detail": detail})
+ return _status(slot, "dev", keys, rows,
+ reviewable=bool(_one(keys, "collected")), pages=pages)
+
+
+def cortex_status(slot: str, keys: dict, members: list, states: dict,
+ live: dict, pages: str) -> dict | None:
+ """The Cortex's (science) reading — a rolling board, not one sitting.
+
+ A science slot stays open while anything is still ON the board: members
+ join it as their runs finish and leave it as they are ruled. `states` is
+ the phase file's own `State:` per slug (the census join — the phase file
+ is the state, the record is only how the member got here) and `live` the
+ per-slug progress note. Members named on a `- carried:` line are excluded
+ from the open test: they belong to the next board.
+ """
+ carried = _carried_slugs(keys, members)
+ rows, on_board = [], False
+ for m in members:
+ slug = m.get("slug", "")
+ state = (states.get(slug) or m.get("state") or "").strip().lower()
+ if state in ("submitted", "running"):
+ row = (IN_PROGRESS, live.get(slug, ""))
+ elif state == "pulled":
+ row = (PULLED, "")
+ elif state == "awaiting-ruling":
+ row = (AWAITING, "")
+ elif state in ("accepted", "rerun", "dropped"):
+ row = (RULED, state)
+ else:
+ row = (RULED, state) if state else (NOT_DELIVERED, "")
+ if state in CORTEX_BOARD_STATES and slug not in carried:
+ on_board = True
+ rows.append({"slug": slug, "state": row[0], "detail": row[1]})
+ if not on_board:
+ return None
+ return _status(slot, "cortex", keys, rows,
+ reviewable=any(r["state"] == AWAITING for r in rows),
+ pages=pages)
+
+
+def _status(slot: str, kind: str, keys: dict, rows: list, reviewable: bool,
+ pages: str) -> dict:
+ return {
+ "slot": slot,
+ "kind": kind,
+ "dispatched": _one(keys, "dispatched"),
+ "members": rows,
+ "in_progress": sum(1 for r in rows if r["state"] == IN_PROGRESS),
+ "awaiting": sum(1 for r in rows if r["state"] == AWAITING),
+ "reviewable": reviewable,
+ "url": review_url(pages, slot) if reviewable else "",
+ "packet": f"batches/packets/{slot}.html",
+ }
+
+
+def pick_slot(rows: list) -> dict | None:
+ """The one slot the box shows: the OPEN record with the latest dispatch.
+
+ `rows` are the per-record readings (`None` for a record that is closed).
+ Dispatch order, never the record NAME's order: `-night` sorts before `-pm`
+ on the same date, so a lexical pick shows yesterday's evening slot while
+ tonight's runs are still on the board.
+ """
+ live = [r for r in rows if r]
+ if not live:
+ return None
+ return max(live, key=lambda r: (r["dispatched"], r["slot"]))
+
+
+def review_url(pages: str, slot: str) -> str:
+ """The packet's Pages URL, `''` when the site URL is underivable."""
+ return f"{pages}packets/{slot}.html" if pages else ""
+
+
+# ------------------------------------------------------------ the renders ---
+def _headline(st: dict) -> str:
+ counts = [f"{st['in_progress']} in progress", f"{st['awaiting']} awaiting "
+ "review"]
+ return f"Batch {st['slot']} — " + " · ".join(counts)
+
+
+def render_html(st: dict | None) -> str:
+ """The box as one `verdict` banner — the theme's existing class."""
+ if st is None:
+ return f'{FIXTURE}
'
+ esc = _html.escape
+ tone = "warn" if st["reviewable"] else "ok"
+ H = [f'{esc(_headline(st))}']
+ for r in st["members"]:
+ line = (f'
{esc(r["slug"])}'
+ f'{pills((r["state"], _TONE.get(r["state"], "n")))}')
+ if r["detail"]:
+ line += f'
— {esc(r["detail"])}'
+ H.append(f"
{line}
")
+ if st["url"]:
+ H.append(f'
Review this batch '
+ "→
")
+ elif st["reviewable"]:
+ # Reviewable, but no Pages site to link into: name the packet by path
+ # rather than render a button that goes nowhere.
+ H.append(f'
Review packet: '
+ f'{esc(st["packet"])}
')
+ else:
+ H.append('
Nothing to review yet.
')
+ return "".join(H) + "
"
+
+
+def render_md(st: dict | None) -> str:
+ """The same box in markdown, as a blockquote — the same words, in the
+ same order, so the two versions of a board read as one page."""
+ if st is None:
+ return f"> **{FIXTURE}**"
+ L = [f"> **{_headline(st)}**", ">"]
+ for r in st["members"]:
+ line = f"> - `{r['slug']}` — {r['state']}"
+ if r["detail"]:
+ line += f" — {r['detail']}"
+ L.append(line)
+ L.append(">")
+ if st["url"]:
+ L.append(f"> [Review this batch →]({st['url']})")
+ elif st["reviewable"]:
+ L.append(f"> Review packet: `{st['packet']}`")
+ else:
+ L.append("> _Nothing to review yet._")
+ return "\n".join(L)
diff --git a/agents/conductors/cortex/_cortex.py b/agents/conductors/cortex/_cortex.py
index ee136e0e..b3def164 100644
--- a/agents/conductors/cortex/_cortex.py
+++ b/agents/conductors/cortex/_cortex.py
@@ -73,6 +73,16 @@
JS as _THEME_JS, boards_footer, css as _theme_css, hero, pills, stats,
)
+# The batch-status box: one reading of "is a batch in flight", shared with
+# the Mind's dashboard (agents/conductors/batch/_status.py). Same import
+# shape as `_theme` above, for the same reason — this page renders with only
+# Brain + Cortex checked out, and `_status` is stdlib-and-`_theme`-only.
+sys.path.insert(0, str(BRAIN_HOME / "agents" / "conductors" / "batch"))
+from _status import ( # noqa: E402
+ cortex_status, pick_slot, read_record,
+ render_html as _status_box_html, render_md as _status_box_md,
+)
+
THEME_ORGAN = "cortex" # whose logo this page wears
CORTEX_REPO = "PyAutoCortex" # an organ name, not an instance fact
@@ -262,42 +272,14 @@ def _int(value: str) -> int | None:
return int(v) if v.isdigit() else None
-# The batch record's own grammar, read here rather than through the Cortex
+# The batch record's own grammar — `RECORD_KEY` lives in `_status.py` now
+# (`read_record`, imported above), read here rather than through the Cortex
# script's `_record_members`: that helper keeps only the FIRST value of a
# repeated key, and `- refreshed:` is repeated once per pull — which is
# exactly the live-progress signal this board wants.
-RECORD_KEY = re.compile(r"^- ([a-z][a-z0-9-]*):(?:\s+(.*?))?\s*$")
REFRESHED = re.compile(r"^(?P\S+)(?:\s+[—-]+\s+(?P.*))?$")
-def read_record(text: str, member_re=None) -> dict:
- """One batch record as `{"keys": {k: [v, …]}, "members": [row, …]}`.
-
- Every value of every key is kept, in file order — the lossless reader the
- live-progress strip and (phase 2b) `collect` both need.
- """
- keys: dict[str, list[str]] = {}
- members: list[dict] = []
- in_members = False
- for lineno, raw in enumerate(text.split("\n"), 1):
- m = RECORD_KEY.match(raw)
- if m:
- in_members = m.group(1) == "members"
- keys.setdefault(m.group(1), []).append((m.group(2) or "").strip())
- continue
- if in_members and raw.startswith(" - "):
- row = {"lineno": lineno, "raw": raw}
- mm = member_re.match(raw.replace("--", "—")) if member_re else None
- if mm:
- row.update({k: (mm.groupdict().get(k) or "").strip()
- for k in ("slug", "path", "runs", "minutes", "state")
- if k in mm.groupdict()})
- members.append(row)
- elif raw.strip() and not raw.startswith(" ") and in_members:
- in_members = False
- return {"keys": keys, "members": members}
-
-
def _refresh_index(root: Path, mod) -> dict:
"""`{slug: {"at": …, "note": …, "slot": …}}` — the newest `refreshed:`
line naming each phase slug, across every batch record."""
@@ -431,9 +413,10 @@ def census(root: Path) -> dict:
gated = sorted([r for r in rows if r["state"] == "gated"],
key=lambda r: (r["project"], r["phase"] or 0, r["rel"]))
+ home = _home(root)
return {
"root": str(root),
- "home": _home(root),
+ "home": home,
"generated": _dt.date.today().isoformat(),
"phases": rows,
"by_state": by_state,
@@ -446,6 +429,7 @@ def census(root: Path) -> dict:
"epics": parse_epics(root / "epics.md"),
"batches": [p.stem for p in mod.batch_records(root)],
"reviews": [p.stem for p in mod.batch_reviews(root)],
+ "batch": _slot_status(root, mod, rows, _pages_url(home)),
"problems": project_problems + phase_problems + ruling_problems,
}
@@ -638,6 +622,26 @@ def _live_note(r: dict) -> str:
return " · ".join(bits)
+def _slot_status(root: Path, mod, rows: list, pages: str) -> dict | None:
+ """`c["batch"]` — the batch-status box's one reading.
+
+ A batch record's own `- slug: … — state` is a dispatch-time (or
+ last-refresh-time) snapshot; the phase file's `State:` is what actually
+ happened since. So `states` here is the CENSUS's reading (this run's
+ `rows`, keyed by slug) and `cortex_status` joins each record's members to
+ it rather than trusting the record's inline word. `live` is `_live_note`
+ per slug, the same live-progress text the "live" section already shows.
+ """
+ states = {r["slug"]: r["state"] for r in rows}
+ live = {r["slug"]: _live_note(r) for r in rows}
+ readings = []
+ for path in mod.batch_records(root):
+ rec = read_record(path.read_text(encoding="utf-8"), mod.MEMBER_RE)
+ readings.append(cortex_status(path.stem, rec["keys"], rec["members"],
+ states, live, pages))
+ return pick_slot(readings)
+
+
def _gate_note(r: dict) -> str:
bits = [", ".join(r["gates"]) or "no refs"]
if r["gate_override"]:
@@ -697,6 +701,8 @@ def h2(key: str) -> list:
L += [f"> - `{_cell(p)}`" for p in c["problems"][:10]]
L += [""]
+ L += [_status_box_md(c["batch"]), ""]
+
L += h2("awaiting")
L += _items([_task_row(_phase_head(r)
+ (" — ⚠️ " + _summary_label(
@@ -846,6 +852,8 @@ def phase_head(r, tone_pills=()):
H += [f"{_attr(p)}" for p in c["problems"][:10]]
H += [""]
+ H.append(_status_box_html(c["batch"]))
+
H.append(h2("awaiting"))
for r in c["awaiting"]:
tone = ("failures", "r") if r["failed_runs"] else (r["state"], "y")
diff --git a/agents/conductors/intake/_intake.py b/agents/conductors/intake/_intake.py
index 80cfc2c4..063d367d 100755
--- a/agents/conductors/intake/_intake.py
+++ b/agents/conductors/intake/_intake.py
@@ -55,6 +55,19 @@
JS as _THEME_JS, boards_footer, css as _theme_css, hero, pills, stats,
)
+# The batch-status box: one reading of "is a batch in flight", shared with
+# the Cortex's dashboard (agents/conductors/batch/_status.py). Not `_batch`
+# itself: its `read_record` has no `member_re` param and its member grammar
+# (outcomes hold spaces) needs `_batch.py`'s own post-split, which `_status`'s
+# generic groupdict extraction cannot carry — see `_dev_member` below — and
+# `_batch.py` is a live edit surface this render path should not couple to.
+sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "agents"
+ / "conductors" / "batch"))
+from _status import ( # noqa: E402
+ dev_status, pick_slot, read_record,
+ render_html as _status_box_html, render_md as _status_box_md,
+)
+
THEME_ORGAN = "mind" # whose logo this page wears
# --- work-type classification -------------------------------------------------
@@ -1612,6 +1625,7 @@ def _count(key):
# Render-only, and never fatal: a Mind with no Cortex beside it draws
# no badge and says nothing about it.
"cortex_gates": cortex_gates(mind),
+ "batch": _batch_status(mind),
}
c["recent"] = recent_events(c)
return c
@@ -1643,6 +1657,57 @@ def _pages_url(home: str) -> str:
return f"https://{m.group(1).lower()}.github.io/{m.group(2)}/" if m else ""
+# The dev batch record's member grammar — duplicated from
+# `agents/conductors/batch/_batch.py`'s `MEMBER_RE`/`parse_member`/`_dash`
+# (not imported: see the `_status` import above). ` - : —
+# — — `; the outcome is split last and kept whole
+# however long it runs, because unlike the Cortex's grammar every real dev
+# outcome sentence contains spaces.
+_DEV_MEMBER_RE = re.compile(r"^ - (?P[^:]+): (?P.+)$")
+
+
+def _dev_member(raw: str) -> dict | None:
+ """One dev member row, or `None` when the line is not this grammar."""
+ m = _DEV_MEMBER_RE.match(raw.replace(" -- ", " — "))
+ if not m:
+ return None
+ fields = [f.strip() for f in m.group("rest").split(" — ", 3)]
+ if len(fields) != 4:
+ return None
+ path, tier, minutes, outcome = fields
+ if not path or " " in path:
+ return None
+ return {"slug": m.group("slug").strip(), "path": path, "tier": tier,
+ "minutes": minutes, "outcome": outcome}
+
+
+def _batch_status(mind: Path) -> dict | None:
+ """`c["batch"]` — the batch-status box's reading of the Mind's side.
+
+ Records live at `/batches/*.md`; a slot closes once it has been
+ reviewed — `reviewed-at:` stamped in the record, or a
+ `/batches/reviews/.md` file on disk (`dev_status` checks
+ both). The review button appears once `collect` has stamped `collected:`
+ — that is when `batches/packets/.html` exists to press it on.
+ """
+ batches = mind / "batches"
+ if not batches.is_dir():
+ return None
+ pages = _pages_url(_mind_home(mind))
+ readings = []
+ for path in sorted(batches.glob("*.md")):
+ text = path.read_text(encoding="utf-8", errors="replace")
+ rec = read_record(text)
+ members = []
+ for row in rec["members"]:
+ parsed = _dev_member(row["raw"])
+ members.append({**row, **parsed} if parsed else row)
+ review_exists = (batches / "reviews" / f"{path.stem}.md").is_file()
+ readings.append(dev_status(path.stem, rec["keys"], members,
+ review_exists, pages))
+ return pick_slot(readings)
+
+
# --------------------------------------------------------- the Cortex badge ---
# A development task that a science phase is gated on has a second reader
# waiting on it, and nothing on this page said so: the Cortex's own board knows
@@ -2116,6 +2181,7 @@ def render_dashboard(c: dict) -> str:
"prompt never advanced, so it still renders as backlog:", ""]
L += [f"> - `{d}`" for d in c["drift"]]
L += [""]
+ L += [_status_box_md(c["batch"]), ""]
L += ["## Start here", ""]
# Epic members never appear in the pick lists or the work-type sections —
@@ -2417,6 +2483,7 @@ def record_row(r):
"whose body records a fix PR (done, never advanced):
", ""]
H += [f"{_attr(d)} " for d in c["drift"]]
H += ["
"]
+ H.append(_status_box_html(c["batch"]))
H += ["Start here
"]
members = _epic_members(c)
diff --git a/board/_theme.py b/board/_theme.py
index 14c4289d..54e5348b 100644
--- a/board/_theme.py
+++ b/board/_theme.py
@@ -534,6 +534,11 @@ def mark(key):
.boards a{display:inline-block;padding:.3rem .7rem;border-radius:999px;
font-size:.82em;font-weight:600;border:1px solid currentColor;opacity:.85}
.boards a:hover{opacity:1;text-decoration:none}
+/* --- the review button: the batch-status box's `_status.render_html` ---- */
+a.go{display:inline-block;margin-top:.15rem;padding:.35rem .8rem;
+ border-radius:999px;font-size:.85em;font-weight:600;color:#fff;
+ background:var(--warn)}
+a.go:hover{opacity:.9;text-decoration:none}
.mdsrc{font-size:.85em}
"""
diff --git a/tests/test_batch_kinds.py b/tests/test_batch_kinds.py
index 4fe91e97..a9ee402f 100644
--- a/tests/test_batch_kinds.py
+++ b/tests/test_batch_kinds.py
@@ -404,14 +404,17 @@ def test_carried_members_ride_onto_the_next_record_unnamed(two_organ, capsys):
def test_the_close_leg_fills_never_overwrites_and_carries_once(
two_organ, capsys):
- """A submitted review closes the board: the packet is never rewritten (it
- and the review file are the audit pair), the close fields are filled where
- the human left them blank, and every member still running is carried."""
+ """A review does not freeze the packet — a Cortex slot is a rolling board
+ (`batches/AGENTS.md`, "The first review does not close the slot"), so it
+ keeps refreshing — but a RULED member's own section is never re-spliced
+ (its packet span is what the human was shown), the close fields are
+ filled where the human left them blank, and every member still running is
+ carried."""
set_run_state(two_organ, "phases/example/11_healthy.md", "running")
assert cli(two_organ, "collect", "--slot", AM, "--apply",
"--stamp", STAMP) == _batch.RC_FINDINGS
capsys.readouterr()
- frozen = packet_text(two_organ)
+ before = spans(packet_text(two_organ))
(two_organ["root"] / "batches" / "reviews" / f"{AM}.md").write_text(
f"# Batch review {AM}\n\n"
@@ -427,10 +430,15 @@ def test_the_close_leg_fills_never_overwrites_and_carries_once(
assert cli(two_organ, "collect", "--slot", AM, "--apply",
"--stamp", "2026-09-03T16:00Z") == _batch.RC_FINDINGS
out = capsys.readouterr().out
- assert "never rewritten after a review" in out
- assert packet_text(two_organ) == frozen
+ assert "packet refreshed" in out
+ after = spans(packet_text(two_organ))
+ # the two now-ruled members are off the board (`_on_the_board`) and their
+ # sections are exactly what the human was shown, byte for byte
+ assert after["m-12_resumed"] == before["m-12_resumed"]
+ assert after["m-01_partial"] == before["m-01_partial"]
text = record_text(two_organ)
assert f"- review: batches/reviews/{AM}.md" in text
+ assert text.count("- review:") == 1, "one line — this is the only sitting"
assert "- reviewed-at: 2026-09-03T15:00Z" in text
assert "- review-minutes-actual: 21" in text
assert "- collected: 2026-09-03T12:00Z" in text, "history, not this run"
@@ -440,10 +448,178 @@ def test_the_close_leg_fills_never_overwrites_and_carries_once(
"--stamp", "2026-09-03T17:00Z") == _batch.RC_FINDINGS
out = capsys.readouterr().out
assert record_text(two_organ).count("- carried: 11_healthy") == 1
+ assert record_text(two_organ).count("- review:") == 1, "no new sitting"
assert "already ruled in this slot's review" in out
assert check(two_organ) == []
+def test_the_offered_review_path_is_the_next_free_one(two_organ, capsys):
+ """`organ_for`'s `review_path` is what `packet.js`'s `REVIEW_PATH` and the
+ submit button's GitHub link are built from — `.md` for the first
+ sitting, then the smallest free `-r.md`, N >= 2
+ (`batches/reviews/AGENTS.md`: `-r1` is not a name)."""
+ reviews = two_organ["root"] / "batches" / "reviews"
+ organ = _batch.organ_for("cortex", two_organ["root"], AM)
+ assert organ["review_path"] == f"batches/reviews/{AM}.md"
+
+ (reviews / f"{AM}.md").write_text(f"# Batch review {AM}\n",
+ encoding="utf-8")
+ organ = _batch.organ_for("cortex", two_organ["root"], AM)
+ assert organ["review_path"] == f"batches/reviews/{AM}-r2.md"
+
+ (reviews / f"{AM}-r2.md").write_text(f"# Batch review {AM}-r2\n",
+ encoding="utf-8")
+ organ = _batch.organ_for("cortex", two_organ["root"], AM)
+ assert organ["review_path"] == f"batches/reviews/{AM}-r3.md"
+
+ # `packet.js` reads `REVIEW_PATH` — the templated page needs no change
+ js = (BRAIN / "agents" / "conductors" / "batch" / "packet.js").read_text(
+ encoding="utf-8")
+ assert 'var REVIEW_PATH = "%%REVIEW_PATH%%"' in js
+ assert cli(two_organ, "collect", "--slot", AM, "--apply",
+ "--stamp", STAMP) == _batch.RC_FINDINGS
+ capsys.readouterr()
+ assert (f'var REVIEW_PATH = "batches/reviews/{AM}-r3.md"'
+ in packet_text(two_organ))
+
+
+def test_a_second_partial_review_refreshes_the_packet_untouched_elsewhere(
+ two_organ, capsys):
+ """A rolling slot's second sitting lands as `-r2.md`
+ (`batches/reviews/AGENTS.md`) and the packet keeps refreshing under it —
+ the member neither sitting touched is exactly what the first refresh
+ rendered, byte for byte."""
+ assert cli(two_organ, "collect", "--slot", AM, "--apply",
+ "--stamp", STAMP) == _batch.RC_FINDINGS
+ capsys.readouterr()
+ reviews = two_organ["root"] / "batches" / "reviews"
+ pass0 = spans(packet_text(two_organ))
+
+ (reviews / f"{AM}.md").write_text(
+ f"# Batch review {AM}\n\n"
+ f"- packet: batches/packets/{AM}.html\n"
+ "- reviewed-at: 2026-09-03T15:00Z\n\n"
+ "## 12_resumed — FAILED\n- decision: rerun\n- ruled: yes\n\n"
+ "It resumed the previous fit.\n", encoding="utf-8")
+ assert cli(two_organ, "collect", "--slot", AM, "--apply",
+ "--stamp", "2026-09-03T16:00Z") == _batch.RC_FINDINGS
+ capsys.readouterr()
+ first_pass = spans(packet_text(two_organ))
+ # ruled — its span is still on the page (a correction is a new dated
+ # page, not an edit), and it is EXACTLY what pass0 rendered: not re-spliced
+ assert first_pass["m-12_resumed"] == pass0["m-12_resumed"]
+
+ (reviews / f"{AM}-r2.md").write_text(
+ f"# Batch review {AM}-r2\n\n"
+ f"- packet: batches/packets/{AM}.html\n"
+ "- reviewed-at: 2026-09-03T18:00Z\n\n"
+ "## 01_partial — SUSPECT\n- decision: accept\n- ruled: yes\n\n"
+ "Good enough.\n", encoding="utf-8")
+ # every member but 11_healthy (HEALTHY) is now ruled — nothing left needs
+ # a finding
+ assert cli(two_organ, "collect", "--slot", AM, "--apply",
+ "--stamp", "2026-09-03T19:00Z") == _batch.RC_OK
+ out = capsys.readouterr().out
+ assert "packet refreshed" in out
+ second_pass = spans(packet_text(two_organ))
+ # ruled at the FIRST sitting — untouched by the second refresh too
+ assert second_pass["m-12_resumed"] == first_pass["m-12_resumed"]
+ # ruled at the SECOND sitting — frozen at what the first refresh rendered
+ assert second_pass["m-01_partial"] == first_pass["m-01_partial"]
+ # the member neither sitting rules on keeps being re-spliced, unchanged
+ assert second_pass["m-11_healthy"] == first_pass["m-11_healthy"] \
+ == pass0["m-11_healthy"]
+
+ text = record_text(two_organ)
+ assert f"- review: batches/reviews/{AM}.md" in text
+ assert f"- review: batches/reviews/{AM}-r2.md" in text
+ assert text.count("- review:") == 2, "one line per sitting"
+
+
+def test_a_ruled_member_is_not_re_spliced_even_if_its_phase_changes(
+ two_organ, capsys):
+ """`_on_the_board` drops a ruled member from `d["members"]` for good — its
+ packet span is what the human was shown, not a live view, so a change to
+ the underlying phase file AFTER the ruling still leaves the rendered
+ section exactly as it was."""
+ assert cli(two_organ, "collect", "--slot", AM, "--apply",
+ "--stamp", STAMP) == _batch.RC_FINDINGS
+ capsys.readouterr()
+ before = spans(packet_text(two_organ))["m-12_resumed"]
+
+ (two_organ["root"] / "batches" / "reviews" / f"{AM}.md").write_text(
+ f"# Batch review {AM}\n\n"
+ f"- packet: batches/packets/{AM}.html\n"
+ "- reviewed-at: 2026-09-03T15:00Z\n\n"
+ "## 12_resumed — FAILED\n- decision: rerun\n- ruled: yes\n\n"
+ "It resumed the previous fit.\n", encoding="utf-8")
+ assert cli(two_organ, "collect", "--slot", AM, "--apply",
+ "--stamp", "2026-09-03T16:00Z") == _batch.RC_FINDINGS
+ capsys.readouterr()
+
+ # mutate the ruled member's own phase file — a live re-score would notice
+ set_run_state(two_organ, "phases/example/12_resumed.md", "running")
+ assert cli(two_organ, "collect", "--slot", AM, "--apply",
+ "--stamp", "2026-09-03T17:00Z") == _batch.RC_FINDINGS
+ out = capsys.readouterr().out
+ assert "already ruled in this slot's review" in out
+ after = spans(packet_text(two_organ))["m-12_resumed"]
+ assert after == before, "ruled — its packet span never moves again"
+
+
+def test_a_slot_closes_when_ruled_or_carried_one_running_stays_open(
+ two_organ, capsys):
+ """`batches/AGENTS.md`: the slot stays open while any non-carried member
+ is still on the board, and closes when nothing is left. A member merely
+ running right now holds the slot open until a sitting actually records it
+ as carried — `_cortex_slot_open` is the same reading `_status.
+ cortex_status` gives the dashboards, replayed against this collect's own
+ `d["members"]` and the record's OWN `- carried:` lines."""
+ assert cli(two_organ, "collect", "--slot", AM, "--apply",
+ "--stamp", STAMP) == _batch.RC_FINDINGS
+ capsys.readouterr()
+ ctx = ctx_of(two_organ)
+ reviews = two_organ["root"] / "batches" / "reviews"
+ rec = _batch.read_record(record_text(two_organ))
+
+ d = _batch.collect(two_organ["root"], AM, kind="cortex", cortex=ctx)
+ assert _batch._cortex_slot_open(d, rec) is True, \
+ "nothing ruled or carried yet — every member holds the board open"
+
+ reviews.mkdir(parents=True, exist_ok=True)
+ (reviews / f"{AM}.md").write_text(
+ f"# Batch review {AM}\n\n"
+ f"- packet: batches/packets/{AM}.html\n"
+ "- reviewed-at: 2026-09-03T15:00Z\n\n"
+ "## 12_resumed — FAILED\n- decision: rerun\n- ruled: yes\n\nRerun.\n\n"
+ "## 01_partial — SUSPECT\n- decision: accept\n- ruled: yes\n\nOK.\n",
+ encoding="utf-8")
+ d_open = _batch.collect(two_organ["root"], AM, kind="cortex", cortex=ctx)
+ assert _batch._cortex_slot_open(d_open, rec) is True, \
+ "11_healthy is still awaiting-ruling and not yet recorded as carried"
+
+ # the SAME board state, but 11_healthy is already recorded (by an earlier
+ # sitting) as carried — nothing left worth waiting for
+ rec_carried = _batch.read_record(
+ record_text(two_organ)
+ + "- carried: 11_healthy — still awaiting-ruling at review\n")
+ assert _batch._cortex_slot_open(d_open, rec_carried) is False, \
+ "the one member left is already carried — not running, not open"
+
+ # …and ruling every member closes it outright, no carry needed at all
+ (reviews / f"{AM}.md").write_text(
+ f"# Batch review {AM}\n\n"
+ f"- packet: batches/packets/{AM}.html\n"
+ "- reviewed-at: 2026-09-03T15:00Z\n\n"
+ "## 11_healthy — HEALTHY\n- decision: accept\n- ruled: yes\n\nGood.\n\n"
+ "## 12_resumed — FAILED\n- decision: rerun\n- ruled: yes\n\nRerun.\n\n"
+ "## 01_partial — SUSPECT\n- decision: accept\n- ruled: yes\n\nOK.\n",
+ encoding="utf-8")
+ d_closed = _batch.collect(two_organ["root"], AM, kind="cortex", cortex=ctx)
+ assert _batch._cortex_slot_open(d_closed, rec) is False, \
+ "every member ruled — nothing left on the board"
+
+
def test_a_record_naming_a_phase_this_cortex_does_not_have_is_a_note(
two_organ, capsys):
record = two_organ["root"] / "batches" / f"{AM}.md"
diff --git a/tests/test_batch_status.py b/tests/test_batch_status.py
new file mode 100644
index 00000000..4661c044
--- /dev/null
+++ b/tests/test_batch_status.py
@@ -0,0 +1,193 @@
+"""tests/test_batch_status.py — the batch-status box's one reading.
+
+`agents/conductors/batch/_status.py` is imported by both dashboards
+(`_cortex.py`, `_intake.py`) and by `_batch.py` for its shared vocabulary.
+This file exercises the module directly, against constructed records rather
+than a live checkout: the box's own rules are organ-agnostic (dev is a
+dispatch-and-review sitting, science is a rolling board) and belong to
+`_status.py` alone, not to either organ's fixture.
+
+Three things this file defends, because getting any of them wrong makes the
+box lie about what a human still has to do:
+
+- The **dev** rule: the button waits for `collected:`, and the box itself
+ disappears once the slot has been reviewed (`reviewed-at:` OR a review file
+ on disk — either says the sitting is over).
+- The **cortex** rule: the board is rolling — a slot stays open while any
+ member is still `submitted`/`running`/`pulled`/`awaiting-ruling`, the
+ button appears as soon as ONE member is `awaiting-ruling`, and a member
+ named on a `- carried:` line does not hold its OWN slot open (it belongs to
+ the next one).
+- `pick_slot` reads `- dispatched:`, never the record's file name — two slots
+ dispatched on the same date sort by clock time, not by the slot label.
+"""
+
+from __future__ import annotations
+
+import importlib.util
+import sys
+from pathlib import Path
+
+BRAIN = Path(__file__).resolve().parents[1]
+_spec = importlib.util.spec_from_file_location(
+ "_status_under_test", BRAIN / "agents" / "conductors" / "batch" / "_status.py")
+_status = importlib.util.module_from_spec(_spec)
+sys.modules["_status_under_test"] = _status
+_spec.loader.exec_module(_status)
+
+PAGES = "https://pyautolabs.github.io/PyAutoMind/"
+
+
+# ------------------------------------------------------------------- dev ---
+def test_dev_in_flight_member_reads_as_in_progress_with_no_button():
+ keys = {"dispatched": ["2026-09-03T18:00Z"]}
+ members = [{"slug": "autofit-resampling-info", "outcome": "RUNNING"}]
+ st = _status.dev_status("2026-09-03-pm", keys, members, review_exists=False,
+ pages=PAGES)
+ assert st is not None
+ assert st["reviewable"] is False
+ assert st["url"] == ""
+ assert st["members"][0]["state"] == _status.IN_PROGRESS
+ assert "Nothing to review yet" in _status.render_md(st)
+ assert 'class="go"' not in _status.render_html(st)
+
+
+def test_dev_collected_slot_gets_the_button():
+ keys = {"dispatched": ["2026-09-03T18:00Z"], "collected": ["2026-09-03T20:00Z"]}
+ members = [{"slug": "autofit-resampling-info",
+ "outcome": "DELIVERED (PyAutoFit#1554)"}]
+ st = _status.dev_status("2026-09-03-pm", keys, members, review_exists=False,
+ pages=PAGES)
+ assert st is not None
+ assert st["reviewable"] is True
+ assert st["url"] == f"{PAGES}packets/2026-09-03-pm.html"
+ assert 'class="go"' in _status.render_html(st)
+ assert "[Review this batch →]" in _status.render_md(st)
+
+
+def test_dev_slot_closes_on_reviewed_at_key():
+ keys = {"dispatched": ["2026-09-03T18:00Z"], "collected": ["2026-09-03T20:00Z"],
+ "reviewed-at": ["2026-09-03T21:00Z"]}
+ members = [{"slug": "autofit-resampling-info", "outcome": "DELIVERED"}]
+ assert _status.dev_status("2026-09-03-pm", keys, members, review_exists=False,
+ pages=PAGES) is None
+
+
+def test_dev_slot_closes_when_a_review_file_exists_even_without_the_key():
+ # `_intake.py`'s adapter passes `review_exists` from the reviews/ folder —
+ # a record transcribed or migrated in may carry no `reviewed-at:` line at
+ # all, and the box must still treat the sitting as over.
+ keys = {"dispatched": ["2026-09-03T18:00Z"], "collected": ["2026-09-03T20:00Z"]}
+ members = [{"slug": "autofit-resampling-info", "outcome": "DELIVERED"}]
+ assert _status.dev_status("2026-09-03-pm", keys, members, review_exists=True,
+ pages=PAGES) is None
+
+
+def test_dev_member_with_no_outcome_is_not_delivered():
+ keys = {"dispatched": ["2026-09-03T18:00Z"]}
+ members = [{"slug": "silent-member", "outcome": ""}]
+ st = _status.dev_status("2026-09-03-pm", keys, members, review_exists=False,
+ pages=PAGES)
+ assert st["members"][0]["state"] == _status.NOT_DELIVERED
+
+
+# --------------------------------------------------------------- cortex ---
+def _cortex_member(slug, state):
+ return {"slug": slug, "state": state}
+
+
+def test_cortex_mixed_board_is_open_and_reviewable_with_no_per_member_control():
+ keys = {"dispatched": ["2026-09-01T09:00Z"]}
+ members = [_cortex_member("05_running_array", "running"),
+ _cortex_member("07_awaiting_ruling", "awaiting-ruling")]
+ states = {"05_running_array": "running", "07_awaiting_ruling": "awaiting-ruling"}
+ live = {"05_running_array": "wall 3:10 of 8:00 (40%)"}
+ st = _status.cortex_status("2026-09-01-pm", keys, members, states, live, PAGES)
+ assert st is not None
+ assert st["reviewable"] is True
+ assert st["url"] == f"{PAGES}packets/2026-09-01-pm.html"
+ rows = {r["slug"]: r for r in st["members"]}
+ assert rows["05_running_array"]["state"] == _status.IN_PROGRESS
+ assert rows["07_awaiting_ruling"]["state"] == _status.AWAITING
+ # Exactly one button for the whole box, never a per-member review control.
+ html = _status.render_html(st)
+ assert html.count('class="go"') == 1
+
+
+def test_cortex_all_ruled_slot_is_closed():
+ keys = {"dispatched": ["2026-09-01T09:00Z"]}
+ members = [_cortex_member("08_accepted", "accepted"),
+ _cortex_member("09_rerun", "rerun"),
+ _cortex_member("10_dropped", "dropped")]
+ states = {"08_accepted": "accepted", "09_rerun": "rerun",
+ "10_dropped": "dropped"}
+ assert _status.cortex_status("2026-09-01-pm", keys, members, states, {},
+ PAGES) is None
+
+
+def test_cortex_carried_member_does_not_reopen_its_own_slot():
+ # The member is CURRENTLY awaiting-ruling (per the phase file's own
+ # `State:`), but the record hands it to the next slot — it must not hold
+ # THIS one open, or a closed batch would render as still in flight.
+ keys = {"dispatched": ["2026-09-02T17:51Z"],
+ "carried": ["refs_v1_positions_on_completion — still submitted "
+ "at review"]}
+ members = [_cortex_member("phase2_nss_mainline_gate_a_reuse", "awaiting-ruling"),
+ _cortex_member("refs_v1_positions_on_completion", "submitted")]
+ states = {"phase2_nss_mainline_gate_a_reuse": "accepted",
+ "refs_v1_positions_on_completion": "awaiting-ruling"}
+ assert _status.cortex_status("2026-09-02-pm", keys, members, states, {},
+ PAGES) is None
+
+
+def test_cortex_carried_line_reads_the_double_hyphen_typed_em_dash():
+ keys = {"dispatched": ["2026-09-02T17:51Z"],
+ "carried": ["refs_v1_positions_on_completion -- still submitted "
+ "at review"]}
+ members = [_cortex_member("refs_v1_positions_on_completion", "submitted")]
+ states = {"refs_v1_positions_on_completion": "awaiting-ruling"}
+ assert _status.cortex_status("2026-09-02-pm", keys, members, states, {},
+ PAGES) is None
+
+
+# ------------------------------------------------------------- pick_slot ---
+def _dev_reading(slot, dispatched):
+ members = [{"slug": "m", "outcome": "RUNNING"}]
+ return _status.dev_status(slot, {"dispatched": [dispatched]}, members,
+ review_exists=False, pages=PAGES)
+
+
+def test_pick_slot_reads_dispatched_not_the_lexical_slot_name():
+ # "night" < "pm" lexically, so a slot-name pick would choose the earlier
+ # afternoon slot even though the evening one dispatched later the same day.
+ pm = _dev_reading("2026-09-01-pm", "2026-09-01T13:00Z")
+ night = _dev_reading("2026-09-01-night", "2026-09-01T23:00Z")
+ assert _status.pick_slot([pm, night]) is night
+ assert _status.pick_slot([night, pm]) is night
+
+
+def test_pick_slot_ignores_closed_records_and_returns_none_when_all_are():
+ assert _status.pick_slot([None, None]) is None
+ pm = _dev_reading("2026-09-01-pm", "2026-09-01T13:00Z")
+ assert _status.pick_slot([None, pm]) is pm
+
+
+# --------------------------------------------------------- md/html parity ---
+def test_md_and_html_agree_on_every_slug_and_the_review_url():
+ keys = {"dispatched": ["2026-09-03T18:00Z"], "collected": ["2026-09-03T20:00Z"]}
+ members = [{"slug": "autofit-resampling-info", "outcome": "DELIVERED"},
+ {"slug": "autonerves-colab-silence", "outcome": "MERGED"}]
+ st = _status.dev_status("2026-09-03-pm", keys, members, review_exists=False,
+ pages=PAGES)
+ md, html = _status.render_md(st), _status.render_html(st)
+ for m in members:
+ assert m["slug"] in md
+ assert m["slug"] in html
+ assert st["url"] in md
+ assert st["url"] in html
+
+
+def test_fixture_when_nothing_is_open():
+ assert _status.render_md(None) == "> **No batch in flight.**"
+ assert _status.render_html(None) == (
+ 'No batch in flight.
')
diff --git a/tests/test_cortex_conductor.py b/tests/test_cortex_conductor.py
index b8a668c0..d5409a54 100644
--- a/tests/test_cortex_conductor.py
+++ b/tests/test_cortex_conductor.py
@@ -158,6 +158,47 @@ def test_the_pages_wear_the_cortex_and_nothing_of_the_mind(skeleton):
assert "PyAutoMind" not in html and "/start_dev" not in html
+def test_the_batch_box_lists_every_member_of_the_open_slot(skeleton):
+ st = _cortex.census(skeleton)["batch"]
+ assert st is not None and st["slot"] == "2026-09-01-pm"
+ assert {r["slug"] for r in st["members"]} == {
+ "05_running_array", "06_pulled", "07_awaiting_ruling",
+ "08_accepted", "09_rerun", "10_dropped"}
+
+
+def test_the_review_button_appears_as_soon_as_one_member_is_awaiting_ruling(skeleton):
+ # The skeleton fixture has no git remote of its own, so `census`'s real
+ # `pages` is `''` and the box falls back to naming the packet — the
+ # button itself is exercised here with an explicit Pages URL, the same
+ # call `census` makes with a real one.
+ mod = _cortex.load_cortex(skeleton)
+ rows = [_cortex._phase_row(mod, ph, {})
+ for ph in mod.load_phases(skeleton)[0]]
+ st = _cortex._slot_status(skeleton, mod, rows,
+ "https://pyautolabs.github.io/PyAutoCortex/")
+ assert st["reviewable"] is True # one member (07) is awaiting-ruling
+ html = _cortex._status_box_html(st)
+ assert html.count('class="go"') == 1 # one button for the whole box
+ running = next(r for r in st["members"] if r["slug"] == "05_running_array")
+ assert running["state"] != "awaiting review" # no review control of its own
+
+
+def test_nothing_open_renders_the_fixture(tmp_skeleton):
+ # Rule every live member on the fixture's one batch off the board — the
+ # slot must close, and the box must fall back to the fixed sentence.
+ for slug, was, now in (("05_running_array", "State: running", "State: accepted"),
+ ("06_pulled", "State: pulled", "State: accepted"),
+ ("07_awaiting_ruling", "State: awaiting-ruling",
+ "State: accepted")):
+ f = tmp_skeleton / "phases" / "example" / f"{slug}.md"
+ f.write_text(f.read_text(encoding="utf-8").replace(was, now, 1),
+ encoding="utf-8")
+ c = _cortex.census(tmp_skeleton)
+ assert c["batch"] is None
+ assert "No batch in flight." in _cortex.render_dashboard(c)
+ assert "No batch in flight." in _cortex.render_dashboard_html(c)
+
+
def test_the_check_compare_ignores_the_date_but_not_the_content(skeleton):
"""Two renders on different days must compare equal — the Mind's
normaliser strips only the generated comment, which is why its refresh
@@ -613,13 +654,13 @@ def test_the_conductor_is_stdlib_only_and_never_imports_the_mind():
elif isinstance(node, ast.ImportFrom) and node.module:
imported.add(node.module.split(".")[0])
assert not imported & {"_sizing", "_intake", "yaml"}, sorted(imported)
- # The whole import list, pinned: stdlib, the one root resolver and the
- # shared theme. Anything else would be a dependency this page cannot
- # carry into the Cortex's own workflow.
+ # The whole import list, pinned: stdlib, the one root resolver, the
+ # shared theme and the batch-status box. Anything else would be a
+ # dependency this page cannot carry into the Cortex's own workflow.
assert imported <= {
"__future__", "argparse", "ast", "datetime", "html", "importlib",
"json", "os", "re", "shutil", "subprocess", "sys", "tempfile",
- "zipfile", "pathlib", "_pyauto_root", "_theme",
+ "zipfile", "pathlib", "_pyauto_root", "_theme", "_status",
}, sorted(imported)
diff --git a/tests/test_intake_dashboard.py b/tests/test_intake_dashboard.py
index 4f680d44..eec12e24 100644
--- a/tests/test_intake_dashboard.py
+++ b/tests/test_intake_dashboard.py
@@ -42,7 +42,7 @@ def _prompt(title, difficulty="medium", autonomy="supervised", priority="normal"
def _mind(root: Path, drafts=None, active=None, registries=None,
- complete=None) -> Path:
+ complete=None, batches=None) -> Path:
for rel, body in (drafts or {}).items():
p = root / "draft" / rel
p.parent.mkdir(parents=True, exist_ok=True)
@@ -57,6 +57,12 @@ def _mind(root: Path, drafts=None, active=None, registries=None,
p.write_text(body, encoding="utf-8")
for name, body in (registries or {}).items():
(root / name).write_text(body, encoding="utf-8")
+ # `rel` is relative to `batches/` itself, so `reviews/.md` lands the
+ # review file the batch-status box's "is this slot reviewed?" check reads.
+ for rel, body in (batches or {}).items():
+ p = root / "batches" / rel
+ p.parent.mkdir(parents=True, exist_ok=True)
+ p.write_text(body, encoding="utf-8")
root.mkdir(parents=True, exist_ok=True)
return root
@@ -1726,3 +1732,80 @@ def test_the_pyauto_cortex_env_var_wins_over_the_sibling(tmp_path, monkeypatch):
flight = _page(mind).split("## In flight")[1].split("## Human review")[0]
assert "→ phases/other/09_far.md" in flight
assert "02_gated" not in flight
+
+
+# --------------------------------------------------------------------------- #
+# the batch-status box: c["batch"], shared with the Cortex's own dashboard
+# --------------------------------------------------------------------------- #
+DEV_RECORD_IN_FLIGHT = """# Batch 2026-09-03 pm
+- dispatched: 2026-09-03T18:00Z
+- members:
+ - autofit-resampling-info: draft/bug/autofit/resampling.md — glance — 3 — RUNNING
+"""
+
+DEV_RECORD_COLLECTED = """# Batch 2026-09-03 pm
+- dispatched: 2026-09-03T18:00Z
+- collected: 2026-09-03T20:00Z
+- members:
+ - autofit-resampling-info: draft/bug/autofit/resampling.md — glance — 3 — DELIVERED (PyAutoFit#1554)
+ - autonerves-colab-silence: draft/feature/autonerves/colab_silence.md — glance — 5 — MERGED
+"""
+
+DEV_RECORD_REVIEWED = """# Batch 2026-09-02 pm
+- dispatched: 2026-09-02T18:00Z
+- collected: 2026-09-02T20:00Z
+- reviewed-at: 2026-09-02T21:00Z
+- members:
+ - autofit-resampling-info: draft/bug/autofit/resampling.md — glance — 3 — DELIVERED
+"""
+
+
+def test_in_flight_batch_reads_as_in_progress_with_no_button(tmp_path):
+ mind = _mind(tmp_path, batches={"2026-09-03-pm.md": DEV_RECORD_IN_FLIGHT})
+ page, html = _page(mind), _html(mind)
+ assert "Batch 2026-09-03-pm" in page
+ assert "autofit-resampling-info" in page and "autofit-resampling-info" in html
+ assert "in progress" in page
+ assert "Nothing to review yet" in page
+ assert 'Review this batch →' in html
+
+
+def test_a_reviewed_batch_disappears_from_the_box(tmp_path):
+ mind = _mind(tmp_path, batches={"2026-09-02-pm.md": DEV_RECORD_REVIEWED})
+ assert "No batch in flight." in _page(mind)
+ assert "No batch in flight." in _html(mind)
+
+
+def test_a_review_file_on_disk_closes_the_slot_without_a_reviewed_at_key(tmp_path):
+ # A migrated/transcribed record can carry no `reviewed-at:` line at all —
+ # the review file's existence must still close the box.
+ mind = _mind(tmp_path, batches={
+ "2026-09-03-pm.md": DEV_RECORD_COLLECTED,
+ "reviews/2026-09-03-pm.md": "# Batch review 2026-09-03-pm\n",
+ })
+ assert "No batch in flight." in _page(mind)
+
+
+def test_empty_mind_renders_the_fixture(tmp_path):
+ mind = _mind(tmp_path)
+ assert "No batch in flight." in _page(mind)
+ assert "No batch in flight." in _html(mind)
+
+
+def test_batch_box_md_and_html_agree_on_slugs_and_the_review_url(tmp_path):
+ mind = _mind(tmp_path, batches={"2026-09-03-pm.md": DEV_RECORD_COLLECTED},
+ registries={"repos.yaml": REPOS_YAML})
+ page, html = _page(mind), _html(mind)
+ url = "https://exampleorg.github.io/PyAutoMind/packets/2026-09-03-pm.html"
+ for slug in ("autofit-resampling-info", "autonerves-colab-silence"):
+ assert slug in page and slug in html
+ assert url in page and url in html
From 8d65b66299aec6415c36b2ad9f8d7465a621c158 Mon Sep 17 00:00:00 2001
From: Jammy2211
Date: Thu, 3 Sep 2026 14:23:01 -0400
Subject: [PATCH 2/2] test(batch): genericise fixture facts for the tenant
firewall (#341)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The three box tests carried the live org's Pages host and a PyAutoFit PR
number as fixture text; the firewall gate reads those as instance facts in
an unlisted file. Neither is load-bearing — `exampleorg.github.io` and
`Widgets#1554`, the convention `test_intake_dashboard.py` already uses.
`repos_sync.py --check --only "tenant firewall (organ code)" --root `
now reports OK; the 181 box tests pass.
Co-Authored-By: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_01SgtS7NhqeziqgxKfSiMzRL
---
tests/test_batch_status.py | 4 ++--
tests/test_cortex_conductor.py | 2 +-
tests/test_intake_dashboard.py | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/tests/test_batch_status.py b/tests/test_batch_status.py
index 4661c044..059f0446 100644
--- a/tests/test_batch_status.py
+++ b/tests/test_batch_status.py
@@ -35,7 +35,7 @@
sys.modules["_status_under_test"] = _status
_spec.loader.exec_module(_status)
-PAGES = "https://pyautolabs.github.io/PyAutoMind/"
+PAGES = "https://exampleorg.github.io/PyAutoMind/"
# ------------------------------------------------------------------- dev ---
@@ -55,7 +55,7 @@ def test_dev_in_flight_member_reads_as_in_progress_with_no_button():
def test_dev_collected_slot_gets_the_button():
keys = {"dispatched": ["2026-09-03T18:00Z"], "collected": ["2026-09-03T20:00Z"]}
members = [{"slug": "autofit-resampling-info",
- "outcome": "DELIVERED (PyAutoFit#1554)"}]
+ "outcome": "DELIVERED (Widgets#1554)"}]
st = _status.dev_status("2026-09-03-pm", keys, members, review_exists=False,
pages=PAGES)
assert st is not None
diff --git a/tests/test_cortex_conductor.py b/tests/test_cortex_conductor.py
index d5409a54..dd639cc4 100644
--- a/tests/test_cortex_conductor.py
+++ b/tests/test_cortex_conductor.py
@@ -175,7 +175,7 @@ def test_the_review_button_appears_as_soon_as_one_member_is_awaiting_ruling(skel
rows = [_cortex._phase_row(mod, ph, {})
for ph in mod.load_phases(skeleton)[0]]
st = _cortex._slot_status(skeleton, mod, rows,
- "https://pyautolabs.github.io/PyAutoCortex/")
+ "https://exampleorg.github.io/PyAutoCortex/")
assert st["reviewable"] is True # one member (07) is awaiting-ruling
html = _cortex._status_box_html(st)
assert html.count('class="go"') == 1 # one button for the whole box
diff --git a/tests/test_intake_dashboard.py b/tests/test_intake_dashboard.py
index eec12e24..ca1fc2ac 100644
--- a/tests/test_intake_dashboard.py
+++ b/tests/test_intake_dashboard.py
@@ -1747,7 +1747,7 @@ def test_the_pyauto_cortex_env_var_wins_over_the_sibling(tmp_path, monkeypatch):
- dispatched: 2026-09-03T18:00Z
- collected: 2026-09-03T20:00Z
- members:
- - autofit-resampling-info: draft/bug/autofit/resampling.md — glance — 3 — DELIVERED (PyAutoFit#1554)
+ - autofit-resampling-info: draft/bug/autofit/resampling.md — glance — 3 — DELIVERED (Widgets#1554)
- autonerves-colab-silence: draft/feature/autonerves/colab_silence.md — glance — 5 — MERGED
"""