diff --git a/.github/workflows/cortex_check.yml b/.github/workflows/cortex_check.yml index 9013189..4f5e2c7 100644 --- a/.github/workflows/cortex_check.yml +++ b/.github/workflows/cortex_check.yml @@ -45,9 +45,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: dependencies + # `cortex.py` reads projects.yaml with PyYAML, so the deps go in before + # the check step, not only before the tests. + run: python3 -m pip install --quiet pytest pyyaml - name: cortex check run: python3 scripts/cortex.py check - name: tests - run: | - python3 -m pip install --quiet pytest pyyaml - python3 -m pytest tests -q + run: python3 -m pytest tests -q diff --git a/.github/workflows/dashboard_refresh.yml b/.github/workflows/dashboard_refresh.yml index a7b2a16..76c9913 100644 --- a/.github/workflows/dashboard_refresh.yml +++ b/.github/workflows/dashboard_refresh.yml @@ -57,8 +57,8 @@ permissions: actions: write # Every workflow in this repo that can push to main shares one queue — -# ledger_merge.yml and gates_grade.yml carry the same group — so two bot writers -# never race for the tip and spend their retries re-merging. Queue rather than +# ledger_merge.yml carries the same group — so two bot writers never race for +# the tip and spend their retries re-merging. Queue rather than # cancel: a cancelled heal can leave a rendered page uncommitted. PR runs never # push, but they share the group rather than carry a second, ref-scoped one; the # queue is short and one name is easier to reason about than two. @@ -79,8 +79,11 @@ jobs: with: repository: PyAutoLabs/PyAutoBrain path: PyAutoBrain - # No setup-python: the conductor and cortex.py are stdlib only, and the - # runner's python3 is the interpreter the laptop render is compared to. + # No setup-python: the runner's python3 is the interpreter the laptop + # render is compared to. PyYAML is `cortex.py`'s one dependency (it reads + # `projects.yaml`), and the conductor imports that script at runtime. + - name: dependencies + run: python3 -m pip install --quiet pyyaml - name: dashboard freshness (self-healing on push to main) working-directory: PyAutoCortex run: | diff --git a/.github/workflows/gates_grade.yml b/.github/workflows/gates_grade.yml deleted file mode 100644 index 5372b4c..0000000 --- a/.github/workflows/gates_grade.yml +++ /dev/null @@ -1,114 +0,0 @@ -name: Gates Grade - -# THE ONE SCHEDULED JOB THAT MUTATES THE LEDGER. Everything else on a timer in -# this repo reads (`cortex_check.yml`) or re-renders a generated page -# (`dashboard_refresh.yml`); this one edits phase files on main. It may only -# ever move a phase `gated → ready` — every gate a phase names has cleared, so -# the work it was waiting on is done — and `ready → gated` when a cleared gate -# reopened. It never rules, never submits, never touches a run line, and never -# moves a phase that has left `ready`: for a `submitted`+ phase a reopened gate -# is reported and nothing is written (`cortex.py gates --grade --write`). -# -# WHY A ROBOT MAY DO THIS. "Is issue #380 closed?" is a fact GitHub answers, not -# a judgement. The human's judgement is the ruling, and no ruling is ever -# written here. A phase arriving in `ready` costs nothing until a human admits -# it into a slot. -# -# FAILS CLOSED. An unreadable ref (a deleted issue, a rate limit, a private -# repo) grades as `unreadable`, that phase is skipped and the script exits 1. -# This job commits whatever DID flip and then exits with that code, so the run -# is red and a human sees the ref — a red job must never mean the flips were -# lost. - -on: - schedule: - - cron: "47 6 * * *" # daily 06:47 UTC — offset off the top of the hour - workflow_dispatch: - -# contents: write → the commit of the flipped phases. -# actions: write → re-dispatch cortex_check.yml and dashboard_refresh.yml: this -# job pushes with GITHUB_TOKEN, which triggers no workflow runs at all, so -# without the explicit dispatches the flipped phases are never checked on main -# and the board keeps showing them as gated. -permissions: - contents: write - actions: write - -# Shared with ledger_merge.yml and dashboard_refresh.yml: every workflow that -# can push to main queues on one name, so two bot writers never race for the tip. -concurrency: - group: cortex-main-writers - cancel-in-progress: false - -jobs: - grade: - # Never in a fork: this job writes to main. - if: github.repository == 'PyAutoLabs/PyAutoCortex' - runs-on: ubuntu-latest - steps: - # Only the Cortex — the grading is `scripts/cortex.py`'s own (stdlib, no - # Brain checkout, no setup-python), so a Brain outage cannot stall the - # gates. Shallow is enough: nothing here reads history. - - uses: actions/checkout@v4 - - - name: grade the gates, commit what flipped - id: grade - env: - # `gh api` reads the gated issues and PRs, which live in the sibling - # PyAuto repos; they are public, so the repo-scoped GITHUB_TOKEN can - # read them. A ref it cannot read is `unreadable` and fails closed. - GH_TOKEN: ${{ github.token }} - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - rc=0 - pushed=no - # Each attempt re-grades on the current tip of main, so a concurrent - # push (the usual cause of a rejected push) never needs a rebase. - for attempt in 1 2 3; do - git fetch origin main - git reset --hard FETCH_HEAD - python3 scripts/cortex.py gates --grade --write | tee grade.txt - rc=${PIPESTATUS[0]} - if git diff --quiet; then - echo "no gate flipped (attempt ${attempt})" - break - fi - # Explicit pathspec: `gates --write` edits phase headers and nothing - # else, and grade.txt is a scratch file that must not be committed. - git add phases - git commit -m "cortex: gates graded — $(grep -c '→ ready' grade.txt || true) flipped" - if git push origin HEAD:main; then - echo "pushed on attempt ${attempt}" - pushed=yes - break - fi - echo "push rejected (attempt ${attempt}) — re-grading on the new tip of main" - done - echo "pushed=${pushed}" >> "$GITHUB_OUTPUT" - echo "rc=${rc}" >> "$GITHUB_OUTPUT" - { - echo '```' - cat grade.txt - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - - # A GITHUB_TOKEN push fires no events, so main's check and the board have - # to be asked by name or the flipped phases sit unchecked and unrendered. - - name: check and re-render main - if: steps.grade.outputs.pushed == 'yes' - env: - GH_TOKEN: ${{ github.token }} - run: | - gh workflow run cortex_check.yml --repo "${{ github.repository }}" --ref main \ - || echo "::warning::could not dispatch cortex_check.yml — its next push-triggered run will check main" - gh workflow run dashboard_refresh.yml --repo "${{ github.repository }}" --ref main \ - || echo "::warning::could not dispatch dashboard_refresh.yml — the board will lag until its nightly run" - - # Last, so an unreadable ref reddens the job only AFTER whatever cleared - # has been committed and published. - - name: fail on an unreadable gate ref - if: steps.grade.outputs.rc != '0' - run: | - echo "::error::'cortex.py gates --grade' exited ${{ steps.grade.outputs.rc }} — a gate ref could not be read; the phases it gates were skipped" - exit ${{ steps.grade.outputs.rc }} diff --git a/.github/workflows/ledger_merge.yml b/.github/workflows/ledger_merge.yml index 27f8842..7f7938b 100644 --- a/.github/workflows/ledger_merge.yml +++ b/.github/workflows/ledger_merge.yml @@ -68,9 +68,9 @@ permissions: # leave a branch merged and undeleted. # # The group is shared by EVERY workflow here that can push to main — -# dashboard_refresh.yml's self-heal and gates_grade.yml's flips carry the same -# name — because the race is not between two merges, it is between two writers -# of the tip. One queue, and each writer sees a settled main. +# dashboard_refresh.yml's self-heal carries the same name — because the race is +# not between two merges, it is between two writers of the tip. One queue, and +# each writer sees a settled main. concurrency: group: cortex-main-writers cancel-in-progress: false @@ -157,6 +157,11 @@ jobs: # header contradicts, a dangling Supersedes:, a run line that does not # parse — nothing heals that automatically, and merging it would put the # contradiction on main. Also the only check audit mode gets to report. + # `cortex.py` reads `projects.yaml` with PyYAML — its one dependency. + - name: dependencies + if: steps.classify.outputs.verdict == 'ledger' + run: python3 -m pip install --quiet pyyaml + - name: Structural drift on the branch (blocking) if: steps.classify.outputs.verdict == 'ledger' run: python3 scripts/cortex.py check diff --git a/.github/workflows/pages_dashboard.yml b/.github/workflows/pages_dashboard.yml index 7785580..45a5f24 100644 --- a/.github/workflows/pages_dashboard.yml +++ b/.github/workflows/pages_dashboard.yml @@ -23,7 +23,6 @@ on: branches: [main] paths: - "dashboard.html" - - "batches/packets/**" workflow_dispatch: permissions: @@ -52,9 +51,6 @@ jobs: run: | mkdir -p _site cp dashboard.html _site/index.html - # Batch review packets — served under /packets/, as the Mind's are. - mkdir -p _site/packets - find batches/packets -maxdepth 1 -name "*.html" -exec cp {} _site/packets/ \; - uses: actions/upload-pages-artifact@v3 with: path: _site diff --git a/AGENTS.md b/AGENTS.md index ed6dad3..b5dc90c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,25 +65,26 @@ human's turn. Full rules in [rulings/AGENTS.md](rulings/AGENTS.md). - **`projects.yaml`** — the science body map. One row per project. **This is code, not ledger**: `sync_cli` and `local_path` are paths a conductor will - execute under, so a change to it is always a human's turn. It is written in a - restricted YAML subset that `cortex.py` parses with the stdlib (REFERENCE.md - "The restricted YAML subset"). + execute under, so a change to it is always a human's turn. `cortex.py` reads + it with PyYAML and validates the fields (REFERENCE.md "projects.yaml"). - **`phases//.md`** — one file per phase, with the Mind's light header (`Key: value` lines, no YAML frontmatter) and a `## Runs` body in the SLURM run-line grammar. `State:` is one of `planned | gated | ready | submitted | running | pulled | awaiting-ruling | accepted | rerun | dropped`. `legacy` and `legacy_wrong` are states of a **run**, never of a phase. - **`rulings///R--.md`** — the ledger of record. -- **`batches/`** — the rolling review board: one record per slot, the archived - packet page under `packets/`, the human's verbatim review under `reviews/`. +- **`batches/`** — closed history. Three 2026-08/09 batch records and the + human's verbatim reviews under `reviews/`, kept because 13 rulings cite them + and append-only for the merge gate: never modified, only added. Nothing + writes new ones (the review-slot apparatus was retired 2026-09-03). - **`epics.md`** — the Cortex half of every split epic; `- mind-half:` names the Mind entry by slug. - **`scripts/cortex.py`** — the one lifecycle script (stdlib only): - `check` — every structural rule, hermetic; `cortex check: OK` or `DRIFT` with one ` - …` line per finding, exit 1. - - `gates [--grade [--write]]` — offline: the gated phases and their refs; - `--grade` polls GitHub and reports which gates cleared; `--write` flips - `gated → ready` (and `ready → gated` on a reopened gate). + - `gates` — read-only and offline: every gated phase, its refs and their + URLs. Nothing polls GitHub and nothing flips a state; a human reads the + listing, opens the refs and types `move ready`. - `rule --body …` — assigns the next ruling id, writes the ruling file(s), updates the phase's `Ruling:` and `State:`. The only door to `accepted`, `rerun`, `dropped` and `leave-to-finish`. @@ -116,50 +117,41 @@ state table. |------|--------------| | `census [--json]` | what the Cortex is holding, by state — the one-screen answer | | `dashboard --check` \| `--apply` | render `dashboard.md` + `dashboard.html`; `--check` exits **1 on drift**, **2 on bad args**, anything else = the renderer could not run | -| `gates [--grade] [--apply]` | the gate refs; `--grade` polls GitHub, `--apply` flips `gated → ready` (and `ready → gated` on a reopen) | -| `plan [--budget N]` | which `ready` phases fit a laptop slot, cheapest first; it hands over a command, never a decision | -| `collect [--slot S] [--pull] [--refreshed ISO] [--apply]` | score a pulled run's legs into a packet member; `--pull` is opt-in and runs the *project's own* sync CLI | - -The batch conductor is the **slot door** over the same two verbs: -`pyauto-brain batch plan --kind cortex` and `batch collect --kind cortex` drive -this `plan` and `collect` for one review slot and write `batches/` — the record, -its `refreshed:` lines and the packet (`batches/AGENTS.md`). +| `gates` | every gated phase, its refs and their URLs — read-only, offline | +| `collect [--phase REL] [--pull] [--refreshed ISO] [--apply]` | score what came back; with no `--phase` it scopes to **every** `submitted \| running` phase, and `--pull` runs the *project's own* sync CLI | **`--apply` here, `--write` there.** The conductor's verbs spell the writing flag `--apply` (the Brain's house spelling, as intake does); `scripts/cortex.py` -spells it `--write`. `cortex gates --grade --apply` is a thin wrapper over -`cortex.py gates --grade --write` — same edit, two doors. +spells it `--write`. -**Nothing here submits a job.** `plan` prints the project's own `sync_cli -submit` line and the `cortex.py move submitted --run ` follow-up; -a human runs both. `collect --pull` is the one leg that shells out, and only to -the project's own CLI. +**Nothing here submits a job.** A human runs the project's own `sync_cli +submit` line and the `cortex.py move submitted --run ` follow-up. +`collect --pull` is the one leg that shells out, and only to the project's own +CLI. ### What runs by itself -Four workflows, and only these may write: +Three workflows, and only these may write: | Workflow | Trigger | May write | |---|---|---| | `cortex_check.yml` | push/PR on ledger, scripts, tests, the dashboards | **nothing** — `cortex.py check` + pytest | | `dashboard_refresh.yml` | push to main, PR, nightly 03:35 UTC, dispatch | `dashboard.md`, `dashboard.html` (self-heal on main; a PR run errors instead of healing) | -| `pages_dashboard.yml` | push to `dashboard.html` / `batches/packets/**`, dispatch | nothing in the repo — it publishes to Pages | -| `gates_grade.yml` | daily 06:47 UTC, dispatch | phase headers — **`State:` and `Gates-cleared:` only**, and only `gated → ready` (or `ready → gated` on a reopened gate) | +| `pages_dashboard.yml` | push to `dashboard.html`, dispatch | nothing in the repo — it publishes to Pages | | `ledger_merge.yml` | push to `claude/**`, dispatch | merges a ledger-only branch to main | -`gates_grade.yml` is **the one scheduled job that mutates the ledger**. It never -rules, never submits, never edits a run line. An unreadable gate ref fails -closed: the phase is skipped, whatever else flipped is still committed, and the -job then goes red so a human sees the ref. +**No scheduled job mutates the ledger.** A daily gate-grading cron did, until +gate grading was retired on 2026-09-03 (2 gated refs, 0 flips in its +lifetime); the only self-healing writer left renders the two generated pages. -All three main-writers share `concurrency: group: cortex-main-writers`, so two +The main-writers share `concurrency: group: cortex-main-writers`, so two bots never race for the tip of main. And because a `GITHUB_TOKEN` push triggers no workflow at all, each of them re-dispatches by name what its push should have woken (`pages_dashboard.yml` after a heal; `cortex_check.yml` + `dashboard_refresh.yml` after a grading commit). The board is published at **** — -`dashboard.html` as the index, archived packets under `/packets/`. +`dashboard.html` as the index. ## The workspace-paths exception @@ -172,7 +164,7 @@ exception is stated in the file's header and here, and nowhere else — a phase file or a ruling points into a project through its `projects.yaml` row, never with a bare absolute path of its own. -## The laptop lane — what is out of scope, and why +## What is out of scope, and why Quoted verbatim from `PyAutoMind/complete/archive/shelved/batch_science_lane.md` so nobody re-derives these: @@ -195,8 +187,9 @@ so nobody re-derives these: > prompts that they are human-driven and supervised with a judged verdict as the > deliverable — **no transport was ever going to make those unattended.** -Every Cortex phase is therefore `Lane: local-dev`, always; the field exists so -the batch conductor's lane filter reads one vocabulary across both surfaces. +Every Cortex phase therefore runs on the laptop. That was once written into +each phase file as `Lane: local-dev` — 32 of 32 said the same thing, so the +header was deleted on 2026-09-03 and the fact lives here instead. ## Hard rules @@ -208,8 +201,8 @@ the batch conductor's lane filter reads one vocabulary across both surfaces. or batch change; `ledger_merge.yml` runs it on the trial-merge tree and a failing check leaves the branch for a human. 5. **No hand-written HTML.** `dashboard.html` is rendered by the cortex - conductor and packets by the batch conductor; a hand edit is drift that - `dashboard_refresh.yml` will overwrite on the next push to `main`. + conductor; a hand edit is drift that `dashboard_refresh.yml` will overwrite + on the next push to `main`. ## Remote sessions (Claude Code on web and mobile) diff --git a/README.md b/README.md index daf53fe..b17f2f2 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ and ends in a **ruling**: it `submitted` and then `running`, with every job id recorded in the file's `## Runs` block in SLURM notation. 3. **Pull and review.** When the results are pulled to the laptop the phase - joins the **rolling review board** (`batches/`); a member joins when its - results are in hand, never mid-flight. + becomes `pulled` and then `awaiting-ruling` — it is waiting on the human's + eyes, and on nothing else. 4. **Rule.** The human's verdict — `accept`, `rerun`, `drop` or `leave-to-finish` — is written into `rulings/` with a permanent id. *A verdict recorded only outside the Cortex does not exist.* @@ -34,18 +34,24 @@ In full those four steps are ten states — `planned`, `gated`, `ready`, `submitted`, `running`, `pulled`, `awaiting-ruling`, then `accepted`, `rerun` or `dropped` — of which only the last three are reachable, and only through `scripts/cortex.py rule` (`legacy` and `legacy_wrong` are states of a *run*, -never of a phase). The review slot itself is opened and refreshed from the -Brain's batch conductor: `pyauto-brain batch plan --kind cortex --apply ---review-at ` writes the record under `batches/`, and `batch collect ---kind cortex` fills the board in as results are pulled. +never of a phase). Checking in is one command from the Brain's cortex +conductor — `pyauto-brain cortex collect --pull --apply` pulls every submitted +or running phase's results, scores them and moves what came back to +`awaiting-ruling`. `batches/` is closed history: the three 2026-08/09 batch +records and the human's verbatim reviews stay readable because 13 rulings cite +them, and nothing writes new ones. The Cortex is **not a second PyAutoMind**: development tasks stay in the Mind and are named here only as gates. Its one script, `scripts/cortex.py`, checks the tree and moves phases between states; the ledger folders auto-merge from any surface that can push. -The schemas — phase files, the run-line grammar, rulings, batches, the -`projects.yaml` subset — are in [REFERENCE.md](REFERENCE.md); how agents should +`scripts/cortex.py` needs Python 3.10+ and **PyYAML** (`python3 -m pip install +pyyaml`), which is how it reads `projects.yaml`; the tests additionally need +`pytest`. Nothing else. + +The schemas — phase files, the run-line grammar, rulings, the `projects.yaml` +fields — are in [REFERENCE.md](REFERENCE.md); how agents should operate this repo is in [AGENTS.md](AGENTS.md); every design choice the birth epic did not fix is dated in [docs/schema_decisions.md](docs/schema_decisions.md). The organism this repo is the Cortex of is described once in diff --git a/REFERENCE.md b/REFERENCE.md index 14615be..0cf46d0 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -19,12 +19,10 @@ Project: example Phase: 5 State: running Gates: PyAutoArray#431 -Gates-cleared: 2026-08-29 Witness: nine of ten array tasks write a sane checkpoint.hdf5 within 8:00 wall Budget: 8:00 Runs: 342091, 342102 Ruling: R-20260901-03 -Lane: local-dev Review-minutes: 8 Epic: example-programme Filed: 2026-08-29 @@ -71,14 +69,11 @@ sections are fixed: `## Question`, `## Witness`, `## Where to look`, `## Runs`, | `Phase:` | integer | unique per project; revival of a dropped phase is a new number | | `State:` | `planned \| gated \| ready \| submitted \| running \| pulled \| awaiting-ruling \| accepted \| rerun \| dropped` | | | `Gates:` | comma-separated GitHub refs | `Repo#N` (owner `PyAutoLabs`) or an issue/PR URL; **no `owner/Repo#N`** form | -| `Gates-cleared:` | `YYYY-MM-DD` | written by `gates --grade --write` when every ref cleared | -| `Gate-override:` | reason | written by `move --override ""` | | `Reset:` | reason | written by `move ready --reason ""` when a `submitted \| running` phase goes back to `ready` | | `Witness:` | free text | **mandatory before `submitted`** — the pre-registered checkable claim | | `Budget:` | `H+:MM` | wall budget per run | | `Runs:` | comma-separated job **stems** | the index of the `## Runs` body; equal to the set of body stems | | `Ruling:` | ruling id | the chain head (see "Rulings") | -| `Lane:` | `local-dev` | always | | `Review-minutes:` | integer | a seed, not a measurement | | `Epic:` | slug | shared with the Mind — the join key across the two dashboards | | `Filed:` | `YYYY-MM-DD` | | @@ -103,11 +98,9 @@ The lookbehind `(?"` (→ writes `Gate-override:`) | -| ready | gated | `gates --grade --write` only, when a cleared gate reopened; refused if `Gate-override:` present; for `submitted`+ phases a reopened gate is *reported*, never enforced | +| gated | ready | `move ready` — a human read `gates`, opened the refs and judged them cleared. No flag, and nothing written but `State:` | +| ready | gated | not an edge: re-gating a `ready` phase is a judgement, so it is a hand edit of the header | | ready | submitted | `Witness:` non-empty AND `--run ` supplied | | ready | pulled | legacy-born phase: every run line is `legacy\|legacy_wrong` (`new --legacy-run` / `move pulled`); `Witness:` still mandatory; every `legacy` run gets a `pulled_to:` (`--pulled-to`, else its own `where:`); refused when no run is `legacy` (nothing to review — `rule drop`) | | submitted / running | same | `--run ` appends a wave, a chained job or a checkpoint resubmit (`resumes:`); state unchanged | @@ -145,9 +138,6 @@ owns. The full table: Offline invariants `check` enforces on top of the table: -- `State ∈ ready..accepted` AND `Gates:` non-empty ⇒ `Gates-cleared:` or - `Gate-override:` present (`ready..accepted` = every state from `ready` - through `accepted` in the order listed above, `rerun` included). - `State ∈ submitted..accepted` (and `rerun`) ⇒ `Witness:` non-empty. - `State ∈ accepted | rerun | dropped` ⇒ `Ruling:` present (those states are reachable only through `rule`). @@ -155,11 +145,10 @@ Offline invariants `check` enforces on top of the table: and its head verb is `leave-to-finish` (the `--partial` case). - `State = gated` ⇒ `Gates:` non-empty (a gate that does not exist cannot clear). -- A header key outside the table is drift (`Gate-cleared:` must not pass as - a silent typo); the five body sections are present; `Lane:`, when present, - is `local-dev`; `Phase:` and `Review-minutes:` are integers, `Budget:` is - `H+:MM`, `Gates-cleared:` and `Filed:` are `YYYY-MM-DD`. A key with an - empty value is read as absent. +- A header key outside the table is drift (`Gate:` must not pass as a silent + typo for `Gates:`); the five body sections are present; `Phase:` and + `Review-minutes:` are integers, `Budget:` is `H+:MM` and `Filed:` is + `YYYY-MM-DD`. A key with an empty value is read as absent. --- @@ -173,7 +162,7 @@ PyAutoCortex/ ├── REFERENCE.md ← this file (schemas + grammars) ├── LICENSE .gitignore │ -├── projects.yaml ← the science body map — CODE, not ledger (restricted YAML subset) +├── projects.yaml ← the science body map — CODE, not ledger (read with PyYAML) ├── epics.md ← the Cortex half of each split epic (`- mind-half:`) ├── dashboard.md ← GENERATED board — cortex conductor (`dashboard --apply`); LEDGER ├── dashboard.html ← GENERATED board, the Pages index; LEDGER @@ -181,25 +170,23 @@ PyAutoCortex/ ├── phases//.md ← one file per phase (LEDGER) ├── rulings/AGENTS.md ← the append-only rule ├── rulings///R--.md ← the ledger of record (LEDGER, append-only) -├── batches/AGENTS.md ← batch-record schema (rolling board) -├── batches/-.md ← one record per slot (LEDGER) -├── batches/packets/{AGENTS.md,TEMPLATE.md,.html} -├── batches/reviews/{AGENTS.md,.md,-r.md} +├── batches/AGENTS.md ← "closed history: never modified, only added" +├── batches/-.md ← the 2026-08/09 records (LEDGER, append-only) +├── batches/reviews/{AGENTS.md,.md} ← the human's verbatim reviews (LEDGER, append-only) │ ├── 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 │ -├── scripts/cortex.py ← check · gates · rule · move · new (stdlib only) -├── scripts/ledger_merge.py ← the default-deny ledger classifier (+ append-only on rulings/) +├── scripts/cortex.py ← check · gates · rule · move · new (stdlib + PyYAML) +├── scripts/ledger_merge.py ← the default-deny ledger classifier (+ append-only on rulings/, batches/) ├── tests/test_cortex.py tests/test_ledger_merge.py -├── tests/fixtures/skeleton/ ← one project, one phase per state, five rulings, one batch — the witness +├── tests/fixtures/skeleton/ ← one project, one phase per state, five rulings — the witness ├── tests/fixtures/empty/ ← an empty map passes `check` │ ├── .github/workflows/cortex_check.yml ← check + pytest on push/PR ├── .github/workflows/ledger_merge.yml ← lands ledger-only `claude/**` branches ├── .github/workflows/dashboard_refresh.yml ← renders the board; self-heals main ├── .github/workflows/pages_dashboard.yml ← publishes dashboard.html to Pages -├── .github/workflows/gates_grade.yml ← daily gate grading — the one scheduled mutator └── .claude/hooks/session-start.sh .claude/settings.json ← GENERATED — repos_sync --write ``` @@ -310,7 +297,7 @@ The human's words, verbatim. | `Runs:` | comma-separated stems, or empty | ⊆ the phase's `Runs:` | | `Ruling:` | `accept \| rerun \| drop \| leave-to-finish` | the verb | | `Supersedes:` | one ruling id | optional; see the chain rules | -| `Batch:` | `-` | optional; the join for rulings filed together | +| `Batch:` | `-` | optional-**historical**; the 2026-08/09 rulings' join, nothing writes new ones | | `Reviewed-at:` | timestamp | | | `Review-minutes-actual:` | integer | | | `Follow-ups:` | comma-separated GitHub refs | `GATE_REF_RE`; **the issue is created before `rule` runs** | @@ -318,8 +305,8 @@ The human's words, verbatim. Body: `## Ruling` — the human's words verbatim; `## Evidence` — pointers. -**One ruling file per phase.** A multi-phase ruling (a REWIND) is N files with -the same body and one `Batch:`; `rule --also ` fans out. +**One ruling file per phase.** A multi-phase ruling (a REWIND) is N `rule` +invocations with the same body — one per phase. **Chain rules** (`check`): @@ -348,76 +335,42 @@ only outside the Cortex does not exist* — is [rulings/AGENTS.md](rulings/AGENT --- -## Batches — the rolling board +## Batches — closed history -One record per slot, `batches/-.md`, plus the archived -packet and the human's verbatim review. Full schema and the fields that are -easy to get wrong: [batches/AGENTS.md](batches/AGENTS.md). +`batches/-.md` and the human's verbatim reviews under +`batches/reviews/` are read-only history: three records from 2026-08/09, cited +by 13 rulings, kept because *a verdict recorded only outside the Cortex does +not exist*. They are **append-only** for the merge gate — never modified, only +added — and `check` no longer has a schema for them: the review-slot apparatus +that produced them (the packet page, the scored members, the review-minute +budget, the `-r` sittings) was retired on 2026-09-03, and nothing writes a +new record. -Member line: - -``` - - : — — — -``` - -`` is the phase file's stem; `` is comma-separated stems or -`none`; `` 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; every `review:` -field must resolve — the key repeats, one line per review file. A review -file's title is `# Batch review ` for its own filename, its batch record -`batches/.md` exists, and every section names a member of it. A rolling -slot's later sittings are `batches/reviews/-r.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/.md`, `…--r.md`): - -```markdown -# Batch review - - -- packet: batches/packets/-.html -- reviewed-at: -- review-minutes-actual: - -### Follow-ups accepted -- - -## — -- decision: accept|rerun|drop|leave-to-finish|(none) -- ruled: yes|no - - -``` - -`Follow-ups accepted` is optional and is `###`, not `##`, because `check` -requires every `##` to name a member of the slot's batch record. - -`` ∈ `HEALTHY | SUSPECT | FAILED | RUNNING`. `(none)` with -`ruled: no` is a member the human left untouched — listed, never silently -dropped. `ruled: yes` requires a verb; a verb with `ruled: no` is a leaning the -human did not tick. The four verbs are the whole vocabulary; -the Mind's `merge | tweak | reject | defer` is the dev surface's and does not -appear here. +A ruling's `Batch:` field is therefore **optional-historical**: where present +it must still name an existing `batches/.md`, and nothing writes a new +one. --- -## The restricted YAML subset (`projects.yaml`) +## `projects.yaml` -Real YAML, parsed with the stdlib by a ~40-line line loop. The subset: +Ordinary YAML, read with `yaml.safe_load` (PyYAML is `cortex.py`'s one +dependency; the hand-rolled restricted-subset parser it replaced was retired +on 2026-09-03, its own parity test having proved it equivalent). The document +is a mapping of project key → field mapping: -``` -: # column 0, ^[a-z][a-z0-9_]*$ - : # exactly two spaces; fixed field set, unknown field = error - sync_verbs: [pull, push] # flow list of bare words — the only list +```yaml +: # ^[a-z][a-z0-9_]*$ + : # fixed field set, unknown field = error + sync_verbs: [pull, push] # a list of bare words note: "free text" # the one optional field ``` -- Scalars are bare unless they contain `#`, `:` or edge spaces, then - `"..."` with no escapes. -- Comments on their own line or after ` #`. Blank lines anywhere. -- No block lists, no nesting beyond the two levels, no anchors, no `---`, no - quoted keys. +What `check` validates is the **fields**, not the syntax: the document is a +mapping of rows, every key matches the key grammar, every field is in the set +below, `sync_verbs` is a list of bare words, and each value passes the rules +listed here. A document PyYAML cannot read is reported as one problem. + - Fields (required): `remote` (`owner/repo` | `none` — PyAutoLabs for active projects; a personal remote is recorded as a fact with a `note:`), `local_path` (absolute laptop path — the Cortex-only exception to the @@ -448,14 +401,13 @@ deny**: | `epics.md` | `projects.yaml`, `README.md`, `AGENTS.md`, `REFERENCE.md`, … | | `dashboard.md`, `dashboard.html` (generated) | **`AGENTS.md` / `TEMPLATE.md` inside a ledger dir** | | | anything unclassified — a new root file, a new top-level folder | -| | **any modification or deletion under `rulings/`** (append-only) | +| | **any modification or deletion under `rulings/` or `batches/`** (append-only) | Three exceptions inside the ledger dirs. Two are the Mind's: a **dot-path** anywhere, and a file pytest would **collect** (`conftest.py`, `test_*.py`, `*_test.py`). The third is the Cortex's own **doctrine carve-out** — -`rulings/AGENTS.md`, `batches/AGENTS.md`, `batches/packets/AGENTS.md`, -`batches/packets/TEMPLATE.md`, `batches/reviews/AGENTS.md` are ledger by -location but instructional by content: they say what an entry may be and what +`rulings/AGENTS.md`, `batches/AGENTS.md` and `batches/reviews/AGENTS.md` are +ledger by location but instructional by content: they say what an entry may be and what every future entry is stamped from, so a change to one is a change to behaviour. Auto-merging a rewrite of `rulings/AGENTS.md` would let a branch edit the rule that governs its own merge. @@ -466,7 +418,10 @@ that moves a phase re-renders them in the same push, and And a fourth Cortex rule on *kind* rather than path: `ledger_merge.py` classifies via `git diff --name-status`, and an `M`, `D` or `R` entry under -`rulings/**` is code. +`rulings/**` or `batches/**` is code. `rulings/` because a ruling is +superseded, never edited; `batches/` since 2026-09-03, when the review-slot +apparatus was retired and its records became history — never modified, only +added. The blocking check is `python3 scripts/cortex.py check` run on the **trial-merge tree** — after `git merge --no-ff`, before `git push` — which is what catches @@ -487,38 +442,27 @@ Stdlib only; `main(argv)`; no import-time side effects; every verb takes `--root ` (default: the repo root the script lives in). Every leg of `check` takes `root: Path`, so tests run it against a `tmp_path` copy. -- **`check`** — every rule in this file: phase headers and states, the gates - invariant, the witness invariant, run lines, ruling ids and chains, the - verb↔state agreement, batch member lines and review files, every project - named by a phase path is a `projects.yaml` key, the `projects.yaml` subset - itself. Hermetic (no network, no git). Output in `lifecycle.py`'s shape — +- **`check`** — every rule in this file: phase headers and states, the witness + invariant, run lines, ruling ids and chains, the verb↔state agreement, every + project named by a phase path is a `projects.yaml` key, and `projects.yaml`'s + own fields. Hermetic (no network, no git). Output in `lifecycle.py`'s shape — `cortex check: OK` or `cortex check: DRIFT` followed by one ` - …` line per finding; exit 1 on drift. -- **`gates [--grade [--write]]`** — offline: list every `gated` phase and its - refs. `--grade`: per ref, - `gh api repos///issues/ --jq '{state, state_reason, merged_at: .pull_request.merged_at, is_pr: (.pull_request != null)}'` - with a stdlib `urllib` fallback (User-Agent `pyautocortex`) when `gh` is - absent. **Cleared** = PR ⇒ `merged_at != null` (closed-unmerged is a dead - gate, reported); issue ⇒ `state == closed` and `state_reason ∈ {completed, - null}` (`not_planned` / `duplicate` are dead gates); anything unreadable - fails closed (reported, nothing flipped, exit 1). `--grade` reports only; - `--write` flips `gated → ready` (writing `Gates-cleared:`) and - `ready → gated` when a cleared gate reopened (removing the stale - `Gates-cleared:`; refused when `Gate-override:` is present; for - `submitted`+ phases a reopened gate is reported, never enforced). The - fetch is injectable (`gates_report(root, fetch=…)`) so the grading is - tested offline. +- **`gates`** — read-only and offline: every `gated` phase, its refs and the + URL each ref resolves to (`GATE_REF_RE` → `gate_url`). Nothing polls GitHub + and nothing flips a state. Grading was retired on 2026-09-03 — in its whole + life it saw 2 gated refs and flipped 0, while schema decision 54 routes + sequencing through prose `Ready when:` lines. A human reads the listing, + opens the refs and types `move ready`. - **`rule --body [--supersedes ] [--batch ] - [--minutes n] [--follow-up ]... [--also ]...`** — assigns the - next id for today, writes the ruling file(s) (one per phase; `--also` fans - out with the same body and batch), updates the phase's `Ruling:` and - `State:` per the table (and appends ` — ` to the phase's + [--minutes n] [--follow-up ]...`** — assigns the next id for today, + writes the ruling file, updates the phase's `Ruling:` and `State:` per the + table (and appends ` — ` to the phase's `## Ruling`); the ruling's `Runs:` is the phase's, its `## Evidence` is the phase's `## Where to look`; refuses to touch an existing ruling; refuses a verb the table does not allow from the phase's state; validates everything - for every phase before writing anything. An `--also` phase in `accepted` - supersedes its own `Ruling:` (the REWIND is N accepted phases). -- **`move [--run ] [--reason ..] [--override ..] + before writing anything. +- **`move [--run ] [--reason ..] [--partial] [--pulled-to ] [--partition ..] [--after ] [--resumes ] [--note ..]`** — the table; refuses every ruling edge with a message naming `rule`. `--run` on `submitted`/`running` appends a @@ -543,17 +487,19 @@ Stdlib only; `main(argv)`; no import-time side effects; every verb takes ## Driving the Cortex — the conductor and the workflows -The Cortex is state plus `cortex.py`. The reasoning over it — the board, the -slot, the daily grading — is the Brain's **cortex conductor**: +The Cortex is state plus `cortex.py`. The reasoning over it — the board and the +check-in — is the Brain's **cortex conductor**: ```bash pyauto-brain cortex census [--json] # what is held, by state pyauto-brain cortex dashboard --check | --apply # render the two pages -pyauto-brain cortex gates [--grade] [--apply] # poll the refs; flip what cleared -pyauto-brain cortex plan [--budget N] [--lane ..] # which ready phases fit a slot -pyauto-brain cortex collect [--slot S] [--pull] [--refreshed ISO] [--apply] [--out F] +pyauto-brain cortex gates # the gated phases and their refs +pyauto-brain cortex collect [--phase REL] [--pull] [--refreshed ISO] [--apply] [--out F] ``` +`collect` with no `--phase` scopes to **every** phase in `submitted | running` +— that is the check-in: pull what came back, score it, move it on. + With no Brain install, or from a workflow: ```bash @@ -565,9 +511,8 @@ verb. (`--cortex . dashboard --check` exits 2.) The root is resolved `--cortex` → `$PYAUTO_CORTEX` → `PyAutoCortex` beside the Brain checkout. **Two spellings of the same flag.** The conductor writes with `--apply` (the -Brain's house spelling); `scripts/cortex.py` writes with `--write`. `cortex -gates --grade --apply` is a thin wrapper over `cortex.py gates --grade ---write`; the edit is `cortex.py`'s either way. +Brain's house spelling); `scripts/cortex.py` writes with `--write`. Every edit +to a phase is `cortex.py`'s either way. **The `--check` exit-code contract** (`dashboard_refresh.yml` depends on it): @@ -588,26 +533,21 @@ for exactly that reason. |---|---|---| | `cortex_check.yml` | push/PR on `phases/ rulings/ batches/ projects.yaml epics.md dashboard.* scripts/ tests/` | nothing (`cortex.py check` + pytest) | | `dashboard_refresh.yml` | push to main + PR on the ledger paths and the two pages, nightly **03:35 UTC**, dispatch | `dashboard.md`, `dashboard.html` on main (3-attempt fetch/reset/render/commit/push); a PR run errors instead of healing | -| `pages_dashboard.yml` | push to `dashboard.html` / `batches/packets/**`, dispatch | nothing in git — publishes `dashboard.html` as the Pages index and `batches/packets/*.html` under `/packets/` | -| `gates_grade.yml` | daily **06:47 UTC**, dispatch | phase headers, `State:` + `Gates-cleared:` only, and only `gated → ready` / `ready → gated` | +| `pages_dashboard.yml` | push to `dashboard.html`, dispatch | nothing in git — publishes `dashboard.html` as the Pages index | | `ledger_merge.yml` | push to `claude/**`, dispatch | merges a ledger-only branch into main | -`gates_grade.yml` is the **one scheduled job that mutates the ledger**. It -grades with `cortex.py gates --grade --write`, commits `phases` with an explicit -pathspec, and an unreadable ref (deleted issue, rate limit, private repo) fails -closed: that phase is skipped, whatever else flipped is still committed and -pushed, and the job then exits non-zero so the run is red. +**No scheduled job mutates the ledger.** A daily gate-grading cron did until +2026-09-03; retiring gate grading retired it with no replacement. Every workflow that can push to main shares `concurrency: group: -cortex-main-writers, cancel-in-progress: false` — `dashboard_refresh.yml`, -`gates_grade.yml`, `ledger_merge.yml` — so two bot writers never race for the -tip. `pages_dashboard.yml` keeps its own `group: pages`. +cortex-main-writers, cancel-in-progress: false` — `dashboard_refresh.yml` and +`ledger_merge.yml` — so two bot writers never race for the tip. +`pages_dashboard.yml` keeps its own `group: pages`. A push made with `GITHUB_TOKEN` triggers **no** workflow, so each writer re-dispatches by name what its push should have woken: `dashboard_refresh.yml` asks for `pages_dashboard.yml` (on both paths — fresh *and* healed; folding the -fresh one away is the bug that stranded the Mind's published board), -`gates_grade.yml` asks for `cortex_check.yml` and `dashboard_refresh.yml`, and +fresh one away is the bug that stranded the Mind's published board), and `ledger_merge.yml` asks for `cortex_check.yml`. The board is published at ****. diff --git a/batches/AGENTS.md b/batches/AGENTS.md index 75b8c0f..dd6699f 100644 --- a/batches/AGENTS.md +++ b/batches/AGENTS.md @@ -1,113 +1,9 @@ -# Batch records — the rolling board - -One file per review slot: `-.md`. This is the ledger of what -was on the board for one slot and what the human ruled — the same genre as the -Mind's batch records, and the evidence base the review-minute estimate is -calibrated from. It is the Mind's schema with what a science batch changes. - -It auto-merges (`scripts/ledger_merge.py` — `batches/` is a ledger dir), -because an unattended system that cannot record its own history unattended -will not record it. - -## A rolling board, not a dispatch - -A dev batch is dispatched at once and reviewed at once. A Cortex batch is a -**live board**: a phase **joins** the review when its results are `pulled`; -nothing in `submitted` or `running` holds review control — those members are -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/-r.md` (N ≥ 2 — the first review is `reviews/.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 ---kind cortex --apply --review-at [--slot ] [--shift -