Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ PyAutoCortex/
├── batches/AGENTS.md ← batch-record schema (rolling board)
├── batches/<YYYY-MM-DD>-<slot>.md ← one record per slot (LEDGER)
├── batches/packets/{AGENTS.md,TEMPLATE.md,<slot>.html}
├── batches/reviews/{AGENTS.md,<slot>.md}
├── batches/reviews/{AGENTS.md,<slot>.md,<slot>-r<N>.md}
│
├── docs/schema_decisions.md ← every dated choice the epic did not fix
├── policy/never_rewrite_history.md policy/remote_sessions.md ← copies of the Mind's; spliced into AGENTS.md
Expand Down Expand Up @@ -363,12 +363,15 @@ Member line:
`<slug>` is the phase file's stem; `<runs>` is comma-separated stems or
`none`; `<state>` is the phase state at the last refresh. `check` verifies
the path exists, the slug matches its stem, the runs ⊆ the phase's `Runs:`,
the review-minutes are an integer and the state is legal; a `review:` field
must resolve. A review file's title is `# Batch review <slot>` for its own
filename, its batch record `batches/<slot>.md` exists, and every section
names a member of it.

**One review grammar** (`batches/reviews/<slot>.md`):
the review-minutes are an integer and the state is legal; every `review:`
field must resolve — the key repeats, one line per review file. A review
file's title is `# Batch review <slot>` for its own filename, its batch record
`batches/<slot>.md` exists, and every section names a member of it. A rolling
slot's later sittings are `batches/reviews/<slot>-r<N>.md` (N ≥ 2); they
resolve to the same record, may be titled for the file or for the slot, and
are checked identically.

**One review grammar** (`batches/reviews/<slot>.md`, `…-<slot>-r<N>.md`):

```markdown
# Batch review <YYYY-MM-DD>-<slot>
Expand Down
16 changes: 14 additions & 2 deletions batches/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ shown as running, and a `leave-to-finish` is the only thing the human can say
about them; the board is the live view of run progress. Each pull appends a
`refreshed:` line — that list is the record of the board filling in.

**The first review does not close the slot.** The human rules on whatever is
reviewable whenever they come in; the slot stays open while any non-carried
member is still `submitted`, `running`, `pulled` or `awaiting-ruling`, and
closes when nothing is left in a board state. Every later sitting lands as
`reviews/<slot>-r<N>.md` (N ≥ 2 — the first review is `reviews/<slot>.md`) and
the record gains another `- review:` line: the field repeats, one line per
file. The live view is the batch status box at the top of both dashboards,
whose button opens the slot's packet on Pages.

## Schema

The record is **opened by the batch conductor** — `pyauto-brain batch plan
Expand All @@ -42,7 +51,8 @@ hand-written record always wrote.
- reviewed-at: 2026-09-01T17:10Z # when they actually sat down
- delivered: <n>/<n>
- packet: batches/packets/<YYYY-MM-DD>-<slot>.html
- review: batches/reviews/<YYYY-MM-DD>-<slot>.md
- review: batches/reviews/<YYYY-MM-DD>-<slot>.md # repeats — one line per review file
- review: batches/reviews/<YYYY-MM-DD>-<slot>-r2.md
- review-minutes-actual: <n>
- carried: <slug> — still <state> at review # one per still-running member, written at close
- notes: |
Expand Down Expand Up @@ -98,4 +108,6 @@ figure is a seed; this is what the slot really cost.
**`packet:` is the page the human actually reviewed, archived** — see
`packets/AGENTS.md`. **`review:` is what the human said, verbatim** — see
`reviews/AGENTS.md`; every ruling `cortex.py rule --batch <slot>` files
traces back to a line in that file.
traces back to a line in one of those files. A rolling slot has more than one:
the field repeats in the order the sittings happened, and `check` resolves
every line.
9 changes: 6 additions & 3 deletions batches/packets/TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,13 @@ lines. `(none)` + `ruled: no` is an untouched member.
Delivery affordances, in this order: **Copy to clipboard** (always works; clipboard API with
`execCommand` fallback), **Download .md** (inert inside a Claude-artifact sandbox — say so in
small print), **Commit on GitHub** — a link to
`https://github.com/PyAutoLabs/PyAutoCortex/new/main?filename=batches/reviews/<slot>.md&value=<urlencoded>`,
`https://github.com/PyAutoLabs/PyAutoCortex/new/main?filename=<review path>&value=<urlencoded>`,
disabled with a use-Copy hint when the URL exceeds 7,500 chars. Close with: "tell the
orchestrator chat 'review submitted'." The committed file lands at
`PyAutoCortex/batches/reviews/<YYYY-MM-DD>-<slot>.md`.
orchestrator chat 'review submitted'." `<review path>` is the **next free** review path for
the slot — `batches/reviews/<YYYY-MM-DD>-<slot>.md` the first time, then
`…-<slot>-r2.md`, `…-r3.md` as the rolling slot is ruled on again; the refresh writes it
into the page's `REVIEW_PATH`, so a re-submitted packet never overwrites the earlier
sitting.

## Design tokens (brief)

Expand Down
19 changes: 14 additions & 5 deletions batches/reviews/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# Batch reviews — what the human ruled, verbatim

One markdown file per slot: `<YYYY-MM-DD>-<slot>.md` — the review the human
submitted from the packet page (its "submit" step produces exactly this file,
committed via the page's GitHub button or pasted to the orchestrator), or the
same content dictated in-chat and transcribed. The batch record's `review:`
field points here.
One markdown file per **sitting**: `<YYYY-MM-DD>-<slot>.md` for the first,
`<YYYY-MM-DD>-<slot>-r<N>.md` (N ≥ 2, one per later sitting) for a rolling
slot the human comes back to — the review they submitted from the packet page
(its "submit" step produces exactly this file, committed via the page's GitHub
button or pasted to the orchestrator), or the same content dictated in-chat
and transcribed. The files are append-only in the sense that matters: a later
one never edits an earlier one, it rules on what the earlier one left open.
The batch record carries one `- review:` line per file.

The orchestrator parses this file at close-out: every ruling it files
(`cortex.py rule <phase> <verb> --batch <slot>`) and every follow-up issue it
Expand Down Expand Up @@ -32,6 +35,12 @@ The human's words, verbatim, or (no note). For rerun this paragraph becomes
the rerun phase's seed; for drop it is the ruling body.
```

A later sitting is the same grammar in a numbered file: its title is either its
own stem (`# Batch review 2026-09-01-pm-r2`) or the slot's (`# Batch review
2026-09-01-pm`), it lists only the members ruled at that sitting, and `check`
resolves it against `batches/<slot>.md` like the first. `-r1` is not a name —
the first review is `<slot>.md`.

`Follow-ups accepted` is optional and is an `###`, not a `##`: `check` requires
every `##` in a review file to name a member of the slot's batch record, so a
non-member section has to sit a level down.
Expand Down
13 changes: 13 additions & 0 deletions docs/schema_decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,3 +366,16 @@ Choices made in phase 2 — the conductor, the board and the workflows
history that only survives in `git log` is a history the next reader will
not find, and two records that disagree are worse than one record and a
pointer.

57. **A rolling slot is ruled more than once: `batches/reviews/<slot>-r<N>.md`,
N ≥ 2** (2026-09-02, PyAutoBrain#341 — after phase 1). The first review
stays `<slot>.md`; every later sitting is a numbered file in the same
grammar, titled for its own stem or for the slot, ruling only on the
members that had landed. The record carries one `- review:` line per file
— the key repeats — and `check` resolves a numbered review to
`batches/<slot>.md` and applies every review rule to it unchanged.
Why: the board was rolling but the review was not. One file per slot meant
the first sitting closed it, so a human who ruled on the two members that
had arrived could only rule on the third by editing a ledger file that is
already someone's evidence. Numbering the sittings keeps each file
append-only and leaves one record resolving them all.
37 changes: 30 additions & 7 deletions scripts/cortex.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@
)
REVIEW_SECTION_RE = re.compile(r"^## (?P<slug>.+?) — (?P<health>[A-Z]+)$")
REVIEW_KEY_RE = re.compile(r"^- (decision|ruled): (.+?)\s*$")
#: the `-r<N>` suffix of a slot's later, partial reviews (`<slot>-r2.md`, N ≥ 2).
REVIEW_PART_RE = re.compile(r"-r(\d+)$")


class CortexError(Exception):
Expand Down Expand Up @@ -784,6 +786,17 @@ def _record_members(text: str) -> "tuple[list[tuple[int, re.Match | None, str]],
return members, keys


def _record_values(text: str, key: str) -> "list[str]":
"""Every non-empty value of a repeatable `- <key>:` line, in file order.
`review:` repeats — one line per review file a rolling slot collected."""
out = []
for raw in text.split("\n"):
m = RECORD_KEY_RE.match(raw)
if m and m.group(1) == key and (m.group(2) or "").strip():
out.append(m.group(2).strip())
return out


def batch_problems(root: Path, phases_by_rel: "dict[str, Phase]") -> "list[str]":
problems: "list[str]" = []
members_by_slot: "dict[str, set[str]]" = {}
Expand All @@ -793,7 +806,7 @@ def batch_problems(root: Path, phases_by_rel: "dict[str, Phase]") -> "list[str]"
if not SLOT_RE.match(slot):
problems.append(f"{rel}: not a slot name (<YYYY-MM-DD>-<slot>.md)")
text = path.read_text(encoding="utf-8")
members, keys = _record_members(text)
members, _ = _record_members(text)
slugs: "set[str]" = set()
for lineno, m, raw in members:
if m is None:
Expand All @@ -817,16 +830,26 @@ def batch_problems(root: Path, phases_by_rel: "dict[str, Phase]") -> "list[str]"
if m.group("state") not in PHASE_STATES:
problems.append(f"{rel}: member {slug}: state '{m.group('state')}' is not a phase state")
members_by_slot[slot] = slugs
review = keys.get("review")
if review and not (root / review).is_file():
problems.append(f"{rel}: review: {review} does not exist")
for review in _record_values(text, "review"):
if not (root / review).is_file():
problems.append(f"{rel}: review: {review} does not exist")
for path in batch_reviews(root):
rel = path.relative_to(root).as_posix()
slot = path.stem
stem = path.stem
# A rolling slot is ruled more than once: the first review is
# `<slot>.md`, every later partial `<slot>-r<N>.md`. They are all the
# same slot's review, so they resolve to the one record. A record whose
# own name ends `-r<N>` wins over the strip.
slot = stem if stem in members_by_slot else REVIEW_PART_RE.sub("", stem)
part = REVIEW_PART_RE.search(stem) if slot != stem else None
if part and int(part.group(1)) < 2:
problems.append(f"{rel}: a partial review is -r2 or later "
f"(the first review of a slot is {slot}.md)")
lines = path.read_text(encoding="utf-8").split("\n")
title = lines[0] if lines else ""
if title != f"# Batch review {slot}":
problems.append(f"{rel}: title must be `# Batch review {slot}`")
titles = [f"# Batch review {stem}"] + ([f"# Batch review {slot}"] if slot != stem else [])
if title not in titles:
problems.append(f"{rel}: title must be " + " or ".join(f"`{t}`" for t in titles))
if slot not in members_by_slot:
problems.append(f"{rel}: no batch record batches/{slot}.md")
section = None
Expand Down
57 changes: 57 additions & 0 deletions tests/test_cortex.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ def test_two_chains_of_one_on_a_phase_are_fine(tmp_path):
# --------------------------------------------------------------------------- #
B = "batches/2026-09-01-pm.md"
V = "batches/reviews/2026-09-01-pm.md"
R2 = "batches/reviews/2026-09-01-pm-r2.md"


def test_batch_member_lines(tmp_path):
Expand Down Expand Up @@ -585,6 +586,62 @@ def test_review_title_and_record_must_exist(tmp_path):
"2026-09-01-pm.md: review: batches/reviews/2026-09-01-pm.md does not exist")


def _partial(root: Path, rel: str, title: str, slug: str = "06_pulled") -> None:
"""A later, partial review of the 2026-09-01-pm slot — one member ruled."""
(root / rel).write_text(
f"# Batch review {title}\n\n"
"- packet: batches/packets/2026-09-01-pm.html\n"
"- reviewed-at: 2026-09-01T21:30Z\n\n"
f"## {slug} — HEALTHY\n"
"- decision: accept\n"
"- ruled: yes\n\n"
"The phase 6 half is in now. Accept.\n")


def test_partial_review_belongs_to_its_slot(tmp_path):
root = _copy(tmp_path)
_partial(root, R2, "2026-09-01-pm-r2")
assert _problems(root) == []
# either title is the slot's: the file's own stem, or the slot it rules on
_partial(root, R2, "2026-09-01-pm")
assert _problems(root) == []
# and the record carries one `- review:` line per file
_edit(root, B, "- review: batches/reviews/2026-09-01-pm.md\n",
f"- review: batches/reviews/2026-09-01-pm.md\n- review: {R2}\n")
assert _problems(root) == []
_edit(root, B, f"- review: {R2}\n", "- review: batches/reviews/2026-09-01-pm-r9.md\n")
_assert_drift(root, "review: batches/reviews/2026-09-01-pm-r9.md does not exist")


def test_partial_review_is_checked_like_any_other(tmp_path):
root = _copy(tmp_path)
_partial(root, "batches/reviews/2026-09-01-pm-r3.md", "2026-09-01-pm-r3", slug="05_running")
_partial(root, "batches/reviews/2026-09-01-pm-r1.md", "2026-09-01-pm-r1")
_partial(root, "batches/reviews/2026-09-05-am-r2.md", "2026-09-05-am-r2")
_partial(root, R2, "2026-09-01-am")
_assert_drift(root, "2026-09-01-pm-r3.md: section '05_running' names no member of "
"batches/2026-09-01-pm.md",
"2026-09-01-pm-r1.md: a partial review is -r2 or later "
"(the first review of a slot is 2026-09-01-pm.md)",
"2026-09-05-am-r2.md: no batch record batches/2026-09-05-am.md",
"2026-09-01-pm-r2.md: title must be `# Batch review 2026-09-01-pm-r2` "
"or `# Batch review 2026-09-01-pm`")


def test_rule_batch_traces_to_a_partial_review(tmp_path):
"""A member the human ruled only in `<slot>-r2.md` is ruled `--batch <slot>`."""
root = _copy(tmp_path)
_partial(root, R2, "2026-09-01-pm-r2")
_edit(root, B, "- review: batches/reviews/2026-09-01-pm.md\n",
f"- review: batches/reviews/2026-09-01-pm.md\n- review: {R2}\n")
_move(root, P["06_pulled"], "awaiting-ruling")
r = _rule(root, P["06_pulled"], "accept", _body(tmp_path), "--batch", "2026-09-01-pm")
assert r.returncode == 0, r.stderr
rf = _fields(root, "rulings/2026/09/R-20260902-01.md")
assert rf["Phase"] == P["06_pulled"] and rf["Batch"] == "2026-09-01-pm"
assert _problems(root) == []


# --------------------------------------------------------------------------- #
# move — every legal transition, every refused one
# --------------------------------------------------------------------------- #
Expand Down
Loading