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 """