From ba582db90ad1581842a4b2de4b401412f8432586 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Tue, 1 Sep 2026 19:32:40 -0400 Subject: [PATCH] cortex: dashboard, pages and gate-grading workflows; ledger carve-out; first render (PyAutoMind#380) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of the cortex-birth epic, Cortex half: the Cortex gets a published board, a nightly self-heal for it, and the one scheduled job allowed to move a phase. Workflows: - dashboard_refresh.yml — renders `dashboard.md`/`dashboard.html` with the Brain's cortex conductor (two checkouts; runner python3, both sides stdlib). PR runs error on drift, push/cron/dispatch self-heal main in a 3-attempt fetch/reset/render/commit/push loop. The `check()` wrapper keeps the exit-code contract: 1 is drift and nothing else, anything higher is a renderer failure and is reported as one. Both dispatch sites for pages_dashboard.yml are kept — the fresh path as well as the heal path, because a GITHUB_TOKEN push fires no events and folding the two together is what stranded the Mind's published board on 2026-08-27. - pages_dashboard.yml — publishes `dashboard.html` as the Pages index and `batches/packets/*.html` under `/packets/`; `enablement: true`, `group: pages`. - gates_grade.yml — daily 06:47 UTC `cortex.py gates --grade --write`. The one scheduled mutator: `gated → ready` (and `ready → gated` on a reopen), phase headers only, explicit `git add phases`. An unreadable ref fails closed and reddens the job AFTER committing whatever flipped, then re-dispatches cortex_check.yml and dashboard_refresh.yml by name. - cortex_check.yml — paths gain the two generated pages; its "no self-heal" sentence now points at dashboard_refresh.yml. - All three main-writers (including ledger_merge.yml, whose group is renamed) share `concurrency: group: cortex-main-writers, cancel-in-progress: false`. Ledger carve-out (scripts/ledger_merge.py + tests): - `AGENTS.md` / `TEMPLATE.md` under a ledger dir are CODE — ledger by location, instructional by content; auto-merging a rewrite of `rulings/AGENTS.md` would let a branch edit the rule that governs its own merge. - `dashboard.md` / `dashboard.html` are LEDGER — generated and self-healed, so the heal commit must land without a human. - Tests: the five doctrine paths are code, every tracked ledger-dir path gets the right verdict, the dashboards are ledger, every workflow file (all five, named) is code, and the workflows are pinned to the spellings they invoke. Docs: AGENTS.md and REFERENCE.md gain "Driving the Cortex" (conductor verbs, the `--apply`/`--write` split, the `--check` exit-code contract, what each workflow may write, the Pages URL); README links the board; schema_decisions.md 38–48. First render committed: `dashboard --check` is clean on the empty tree. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SESCzU1FcwktEAdTZfmf3j --- .github/workflows/cortex_check.yml | 11 +- .github/workflows/dashboard_refresh.yml | 157 +++++++++++++++ .github/workflows/gates_grade.yml | 114 +++++++++++ .github/workflows/ledger_merge.yml | 7 +- .github/workflows/pages_dashboard.yml | 62 ++++++ AGENTS.md | 71 ++++++- README.md | 5 + REFERENCE.md | 109 +++++++++- dashboard.html | 252 ++++++++++++++++++++++++ dashboard.md | 64 ++++++ docs/schema_decisions.md | 88 +++++++++ scripts/ledger_merge.py | 30 ++- tests/test_ledger_merge.py | 76 ++++++- 13 files changed, 1020 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/dashboard_refresh.yml create mode 100644 .github/workflows/gates_grade.yml create mode 100644 .github/workflows/pages_dashboard.yml create mode 100644 dashboard.html create mode 100644 dashboard.md diff --git a/.github/workflows/cortex_check.yml b/.github/workflows/cortex_check.yml index 0961d34..9013189 100644 --- a/.github/workflows/cortex_check.yml +++ b/.github/workflows/cortex_check.yml @@ -3,8 +3,11 @@ name: Cortex Check # Guards the run-and-ruling registry: every phase file parses and its state is # legal, every ruling id is unique and every Supersedes: resolves, every # project a phase names is in projects.yaml, the gates/witness invariants hold -# (`scripts/cortex.py check`), then the script's own tests. No self-heal — -# there is no generated render here; a failing check is a real contradiction. +# (`scripts/cortex.py check`), then the script's own tests. No self-heal here: +# a failing check is a real contradiction and only a human can resolve it. The +# generated pages that DO self-heal (`dashboard.md`, `dashboard.html`) are +# `dashboard_refresh.yml`'s; this workflow only watches them for the drift that +# a hand-edit of a generated page would leave behind. on: push: @@ -15,6 +18,8 @@ on: - "batches/**" - "projects.yaml" - "epics.md" + - "dashboard.md" + - "dashboard.html" - "scripts/**" - "tests/**" - ".github/workflows/cortex_check.yml" @@ -25,6 +30,8 @@ on: - "batches/**" - "projects.yaml" - "epics.md" + - "dashboard.md" + - "dashboard.html" - "scripts/**" - "tests/**" - ".github/workflows/cortex_check.yml" diff --git a/.github/workflows/dashboard_refresh.yml b/.github/workflows/dashboard_refresh.yml new file mode 100644 index 0000000..a7b2a16 --- /dev/null +++ b/.github/workflows/dashboard_refresh.yml @@ -0,0 +1,157 @@ +name: Dashboard Refresh + +# Keeps the generated Cortex pages (`dashboard.md`, linked from the README, and +# `dashboard.html`, the Pages index) in step with the registry they describe. +# The pages are rendered by PyAutoBrain's cortex conductor from `phases/`, +# `rulings/`, `batches/`, `projects.yaml` and `epics.md`, so any push that files +# a phase, moves it, records a run or lands a ruling makes them stale. +# +# Same shape as the Mind's dashboard_refresh.yml, and for the same reason: on +# pull requests a stale page is an error the author fixes on the branch; on +# pushes to main it is SELF-HEALED with a bot commit, because Cortex pushes land +# directly on main from ledger_merge.yml and from branch-scoped sessions, and an +# alarm-only check would just email a human. +# +# The generation stamp AND the visible `Last updated` banner are excluded from +# the drift comparison (`--check`), so a re-render on an unchanged registry is +# not drift and this never commits daily. (The Mind's normaliser strips only the +# comment, which is why its nightly self-heals every night; the Cortex renderer +# strips both — schema_decisions.md 38.) +# +# The nightly cron is the routine rebuild: it catches what push triggers cannot +# — renderer changes merged in PyAutoBrain (the renderer lives there, so no +# Cortex path fires) and any render that depends on the passage of time. A no-op +# night commits nothing. + +on: + schedule: + - cron: "35 3 * * *" + push: + branches: [main] + paths: + - "phases/**" + - "rulings/**" + - "batches/**" + - "projects.yaml" + - "epics.md" + - "dashboard.md" + - "dashboard.html" + pull_request: + paths: + - "phases/**" + - "rulings/**" + - "batches/**" + - "projects.yaml" + - "epics.md" + - "dashboard.md" + - "dashboard.html" + workflow_dispatch: + +# contents: write is needed by the self-heal push on main; PR runs never push. +# actions: write → re-dispatch pages_dashboard.yml after a heal: the heal commit +# is made with GITHUB_TOKEN, which never triggers other workflows, so without +# the explicit dispatch the Pages page stays stale while the repo copy is fresh +# (the Mind was bitten by exactly this on 2026-08-21). +permissions: + contents: write + 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 +# 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. +concurrency: + group: cortex-main-writers + cancel-in-progress: false + +jobs: + refresh: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + path: PyAutoCortex + # The renderer lives with the cortex conductor, not here — the Cortex + # holds the state, the Brain reasons over it (ORGANISM.md). + - uses: actions/checkout@v4 + 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. + - name: dashboard freshness (self-healing on push to main) + working-directory: PyAutoCortex + run: | + BRAIN=../PyAutoBrain/agents/conductors/cortex/_cortex.py + # Exit 1 is drift and nothing else. Any other non-zero code means the + # renderer itself could not run — a Brain/Cortex version skew, say — + # and reporting that as "the page is stale" sends whoever reads the + # log to fix the wrong file. (2 is argparse: the verb or a flag this + # Brain main does not have.) + check() { + local rc=0 + python3 "$BRAIN" dashboard --cortex . --check || rc=$? + if [ "$rc" -gt 1 ]; then + echo "::error::the dashboard renderer exited ${rc} — that is not drift. Check that PyAutoBrain main still provides 'cortex dashboard --check'." + exit 1 + fi + return "$rc" + } + if check; then + # Fresh in the repo does NOT mean published. The commit that made it + # fresh is usually ledger_merge.yml's, pushed with GITHUB_TOKEN, + # which fires no push event -- so pages_dashboard.yml's own + # `push: main, paths: [dashboard.html]` trigger never sees it, and the + # heal-path dispatch below is never reached because we return here. + # That stranded the Mind's published board on 2026-08-27 while main + # was correct, and the nightly cron takes this same path, so nothing + # healed it. Ask the publisher explicitly: it is idempotent and + # collapses under pages_dashboard.yml's `concurrency: group: pages`. + # DO NOT fold this back into the heal path below -- that IS the bug. + if [ "${GITHUB_EVENT_NAME}" != "pull_request" ]; then + GH_TOKEN="${{ github.token }}" gh workflow run pages_dashboard.yml --ref main \ + || echo "::warning::could not dispatch pages_dashboard.yml — the Pages page will lag until its next trigger" + fi + exit 0 + fi + # Only PR runs error-without-healing; push, the nightly schedule and + # manual dispatch all self-heal main. + if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then + echo "::error::dashboard.md/.html are stale — run 'pyauto-brain cortex dashboard --apply' on this branch and commit the result" + exit 1 + fi + echo "the dashboard pages are stale on main — self-healing" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + # Each attempt rebuilds on the current tip of main, so a concurrent + # push (the usual cause of a rejected push) never needs a rebase. + # The heal push uses the default GITHUB_TOKEN, which does not trigger + # workflow runs, so it cannot loop. + for attempt in 1 2 3; do + git fetch origin main + git reset --hard FETCH_HEAD + if check; then + echo "tip of main is already fresh (healed by a concurrent push)" + exit 0 + fi + python3 "$BRAIN" dashboard --cortex . --apply + if ! check; then + echo "::error::'dashboard --apply' did not converge — renderer bug, repair by hand" + exit 1 + fi + git add dashboard.md dashboard.html + git commit -m "cortex: self-heal stale dashboard pages" + if git push origin HEAD:main; then + echo "healed on attempt ${attempt}" + # Token pushes never trigger pages_dashboard.yml — publish the + # healed page explicitly or the Pages site stays stale. + GH_TOKEN="${{ github.token }}" gh workflow run pages_dashboard.yml --ref main \ + || echo "::warning::could not dispatch pages_dashboard.yml — the Pages page will lag until its next trigger" + exit 0 + fi + echo "push rejected (attempt ${attempt}) — retrying on the new tip of main" + done + echo "::error::could not push the healed dashboard after 3 attempts" + exit 1 diff --git a/.github/workflows/gates_grade.yml b/.github/workflows/gates_grade.yml new file mode 100644 index 0000000..5372b4c --- /dev/null +++ b/.github/workflows/gates_grade.yml @@ -0,0 +1,114 @@ +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 7b1992e..27f8842 100644 --- a/.github/workflows/ledger_merge.yml +++ b/.github/workflows/ledger_merge.yml @@ -66,8 +66,13 @@ permissions: # two branches merging concurrently would race, and the loser would spend its # three attempts re-merging. Queue rather than cancel — a cancelled run can # 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. concurrency: - group: cortex-ledger-merge + group: cortex-main-writers cancel-in-progress: false jobs: diff --git a/.github/workflows/pages_dashboard.yml b/.github/workflows/pages_dashboard.yml new file mode 100644 index 0000000..7785580 --- /dev/null +++ b/.github/workflows/pages_dashboard.yml @@ -0,0 +1,62 @@ +name: Pages Dashboard + +# Serves the generated `dashboard.html` — the Cortex board's one-tap-copy twin — +# at this repo's GitHub Pages URL (as the site's index page). +# +# Why a twin exists at all: GitHub's markdown rendering strips JavaScript, so +# `dashboard.md` can never write to the clipboard — on the GitHub mobile app its +# fenced blocks have no copy affordance at all, leaving long-press text +# selection as the only way to grab a phase's command. The HTML page carries a +# real copy button instead: one tap and the command is on the clipboard, ready +# to paste into a Claude Code chat. It is also where a review is done — the +# board is read on a phone as often as on the laptop. +# +# The file itself is rendered by PyAutoBrain's cortex conductor alongside +# `dashboard.md` (one census, two pages) and kept fresh by the same self-heal in +# `dashboard_refresh.yml`; this workflow only publishes it. +# Only the uploaded files reach Pages — nothing else in the repo, and in +# particular not `phases/`, `rulings/` or `projects.yaml`, whose absolute laptop +# paths are not for a public site. + +on: + push: + branches: [main] + paths: + - "dashboard.html" + - "batches/packets/**" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + # enablement: true creates the Pages site on first run, so no manual + # Settings → Pages step is needed. + - uses: actions/configure-pages@v5 + with: + enablement: true + - name: stage the dashboard as the site index + 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 + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/AGENTS.md b/AGENTS.md index acecf79..7dcefc6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,11 +91,70 @@ human's turn. Full rules in [rulings/AGENTS.md](rulings/AGENTS.md). REFERENCE.md; refuses the ruling edges. - `new --phase …` — writes a phase file from the template. +- **`dashboard.md` / `dashboard.html`** — GENERATED, never hand-edited: the + board, rendered by the Brain's cortex conductor and self-healed on `main` + (see "Driving the Cortex" below). They are ledger for the merge gate. - **`scripts/ledger_merge.py`** — the default-deny classifier behind `.github/workflows/ledger_merge.yml`: a `claude/**` push whose whole diff is - ledger (`phases/`, `rulings/`, `batches/`, `epics.md`) lands on `main` by - itself; anything else waits for a human. `python3 scripts/ledger_merge.py - classify --base origin/main` predicts the verdict. + ledger (`phases/`, `rulings/`, `batches/`, `epics.md`, the two generated + dashboards) lands on `main` by itself; anything else waits for a human — and + that "anything else" includes an `AGENTS.md` or `TEMPLATE.md` *inside* a + ledger dir, which is doctrine, not an entry. `python3 + scripts/ledger_merge.py classify --base origin/main` predicts the verdict. + +## Driving the Cortex + +The Cortex holds state and checks itself; it does not reason. The reasoning +lives in the Brain's **cortex conductor** — `pyauto-brain cortex `, or +`python3 PyAutoBrain/agents/conductors/cortex/_cortex.py --cortex +` with no Brain install. It is read-mostly: it renders, it plans and +it scores. The only bytes it writes of its own are the two generated pages; +every change to a *phase* goes through `scripts/cortex.py`, which owns the +state table. + +| Verb | What it does | +|------|--------------| +| `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 | + +**`--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. + +**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. + +### What runs by itself + +Four 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) | +| `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. + +All three 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/`. ## The workspace-paths exception @@ -143,9 +202,9 @@ the batch conductor's lane filter reads one vocabulary across both surfaces. 4. **Run `python3 scripts/cortex.py check` before you push** a phase, ruling 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.** Packets are rendered by the batch conductor - (phase 2 of the birth epic); until then a packet is whatever the session - produced, archived unchanged. +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`. ## Remote sessions (Claude Code on web and mobile) diff --git a/README.md b/README.md index 152ed1f..1786d3c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ body map** (every science project, where it lives, how it syncs) and the **rulings of record** — every verdict a human has passed on a science run, append-only, superseded but never edited. +**The board** — every phase the Cortex is holding, on one page, with the next +command a tap away: (markdown twin: +[dashboard.md](dashboard.md)). Both pages are generated by the Brain's cortex +conductor and refreshed on every push; never hand-edit them. + ## How PyAutoCortex works The unit is a **phase** of a **project**, which spawns **runs** (SLURM job ids) diff --git a/REFERENCE.md b/REFERENCE.md index c0bdca1..f3a4652 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -174,6 +174,8 @@ PyAutoCortex/ │ ├── projects.yaml ← the science body map — CODE, not ledger (restricted YAML subset) ├── 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 │ ├── phases//.md ← one file per phase (LEDGER) ├── rulings/AGENTS.md ← the append-only rule @@ -192,13 +194,17 @@ PyAutoCortex/ ├── tests/fixtures/skeleton/ ← one project, one phase per state, five rulings, one batch — 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/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 ``` -No `skills/` — the Cortex exposes no commands of its own in phase 1; the -conductor that drives it lives in the Brain (phase 2). +No `skills/` — the Cortex exposes no commands of its own; the conductor that +drives it lives in the Brain (`pyauto-brain cortex …`, AGENTS.md "Driving the +Cortex"), and the workflows above are the only things that run it unattended. --- @@ -422,13 +428,27 @@ deny**: |---|---| | `phases/**`, `rulings/**`, `batches/**` | `scripts/`, `tests/`, `.github/`, `policy/`, `docs/` | | `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) | -Two exceptions inside the ledger dirs, as in the Mind: a **dot-path** anywhere, -and a file pytest would **collect** (`conftest.py`, `test_*.py`, `*_test.py`). -The Cortex adds a third: `ledger_merge.py` classifies via `git diff ---name-status`, and an `M`, `D` or `R` entry under `rulings/**` is code. +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 +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. + +Pulling the other way, the two **generated** board pages are ledger: a branch +that moves a phase re-renders them in the same push, and +`dashboard_refresh.yml`'s self-heal commit has to land without a human. + +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. 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 @@ -503,6 +523,79 @@ 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**: + +```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] +``` + +With no Brain install, or from a workflow: + +```bash +python3 ../PyAutoBrain/agents/conductors/cortex/_cortex.py dashboard --cortex . --check +``` + +`--cortex` is a flag of the **subcommand**, not a global one: it follows the +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. + +**The `--check` exit-code contract** (`dashboard_refresh.yml` depends on it): + +| Code | Meaning | What the caller does | +|---|---|---| +| 0 | the committed pages match a fresh render | nothing | +| 1 | **drift** — and nothing else | error on a PR, self-heal on main | +| 2 | bad args — this Brain has no such verb/flag | report a renderer failure | +| other | the renderer could not run (a Brain/Cortex skew) | report a renderer failure | + +Treating 2 as drift sends whoever reads the log to fix `dashboard.md` when the +broken thing is the Brain: `dashboard_refresh.yml` wraps the call in `check()` +for exactly that reason. + +### What each workflow may write + +| Workflow | Trigger | Writes | +|---|---|---| +| `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` | +| `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. + +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`. + +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 +`ledger_merge.yml` asks for `cortex_check.yml`. + +The board is published at ****. + +--- + ## Bootstrap ```bash diff --git a/dashboard.html b/dashboard.html new file mode 100644 index 0000000..19b49be --- /dev/null +++ b/dashboard.html @@ -0,0 +1,252 @@ + + + + + +PyAutoCortex Dashboard + + + + +

PyAutoCortexDashboard

Question. Run. Rule.

Every science phase the Cortex is holding. Tap a phase's 📋 and its next command is on your clipboard. The verdict is always yours — this page hands you the command, never the ruling.

+
  • 0Awaiting ruling
  • 0Running / submitted
  • 0Ready
  • 0Gated
  • 0Recent rulings
+

Last updated 2026-09-01. This page is generated from phases/, rulings/, batches/, epics.md and projects.yaml, so it is only as current as they are. dashboard_refresh.yml re-renders it on every push to main, and the daily gate grading flips cleared gates before it runs.

+

markdown version · GitHub Page

+

Awaiting ruling markdown version

Results are in and nothing is running — the human's verdict is the only thing outstanding. Ordered failures first, then the phases a ruling is required for, then the clean ones.

+

(nothing awaiting a ruling)

+

Running / submitted markdown version

On the queue or on the machine. Wall is what the run lines record at the last refresh, against the phase's own budget.

+

(nothing on the queue)

+

Ready markdown version

Gate cleared, witness registered — these are what cortex plan admits into a laptop slot.

+

(nothing ready to launch)

+

Gated markdown version

Waiting on development work. The daily grading job flips a phase to ready when every reference it names has closed.

+

(nothing gated)

+

Recent rulings markdown version

The ledger of record, newest first. A verdict recorded only outside the Cortex does not exist.

+

(no rulings yet)

+

Epics markdown version

Long-running programmes whose science half lives here; each card links its development half in the Mind.

+

(no epics yet)

+

Projects markdown version

The science body map — where each project lives, what syncs it and where its witness lands.

+

(the science body map is empty)

+ + + + diff --git a/dashboard.md b/dashboard.md new file mode 100644 index 0000000..775a1e6 --- /dev/null +++ b/dashboard.md @@ -0,0 +1,64 @@ +# PyAutoCortex Dashboard + + + +This is the markdown version of the [PyAutoCortex Dashboard](https://pyautolabs.github.io/PyAutoCortex/), which puts a phase's next command on your clipboard with a single tap of 📋. + +Every science phase the Cortex is holding, on one page: what is waiting on your verdict, what is running, what could be launched next and what is still gated on development work. The verdict is always yours — this page hands you the command, never the ruling. + +> **Last updated 2026-09-01.** This page is generated from `phases/`, `rulings/`, `batches/`, `epics.md` and `projects.yaml`, so it is only as current as they are. `dashboard_refresh.yml` re-renders it on every push to `main`, and the daily gate grading flips cleared gates before it runs. + +| Where | Count | +|-------|------:| +| [Awaiting ruling](#awaiting-ruling) | 0 | +| [Running / submitted](#running--submitted) | 0 | +| [Ready](#ready) | 0 | +| [Gated](#gated) | 0 | +| [Recent rulings](#recent-rulings) | 0 | + +## Awaiting ruling + +[markdown version](https://github.com/PyAutoLabs/PyAutoCortex/tree/main/phases/) — Results are in and nothing is running — the human's verdict is the only thing outstanding. Ordered failures first, then the phases a ruling is required for, then the clean ones. + +- _(nothing awaiting a ruling)_ + +## Running / submitted + +[markdown version](https://github.com/PyAutoLabs/PyAutoCortex/tree/main/phases/) — On the queue or on the machine. Wall is what the run lines record at the last refresh, against the phase's own budget. + +- _(nothing on the queue)_ + +## Ready + +[markdown version](https://github.com/PyAutoLabs/PyAutoCortex/tree/main/phases/) — Gate cleared, witness registered — these are what `cortex plan` admits into a laptop slot. + +- _(nothing ready to launch)_ + +## Gated + +[markdown version](https://github.com/PyAutoLabs/PyAutoCortex/tree/main/phases/) — Waiting on development work. The daily grading job flips a phase to ready when every reference it names has closed. + +- _(nothing gated)_ + +## Recent rulings + +[markdown version](https://github.com/PyAutoLabs/PyAutoCortex/tree/main/rulings/) — The ledger of record, newest first. A verdict recorded only outside the Cortex does not exist. + +- _(no rulings yet)_ + +## Epics + +[markdown version](https://github.com/PyAutoLabs/PyAutoCortex/blob/main/epics.md) — Long-running programmes whose science half lives here; each card links its development half in the Mind. + +- _(no epics yet)_ + +## Projects + +[markdown version](https://github.com/PyAutoLabs/PyAutoCortex/blob/main/projects.yaml) — The science body map — where each project lives, what syncs it and where its witness lands. + +- _(the science body map is empty)_ + +--- + +Boards: [Brain](https://pyautolabs.github.io/PyAutoBrain/) · [Mind](https://pyautolabs.github.io/PyAutoMind/) · [Memory](https://pyautolabs.github.io/PyAutoMemory/) · [Heart](https://pyautolabs.github.io/PyAutoHeart/) · [Hands](https://pyautolabs.github.io/PyAutoHands/) · [Organism](https://pyautolabs.github.io/PyAutoScientist/) + diff --git a/docs/schema_decisions.md b/docs/schema_decisions.md index 82ddc81..58971a8 100644 --- a/docs/schema_decisions.md +++ b/docs/schema_decisions.md @@ -147,3 +147,91 @@ Choices made while writing `cortex.py` (slice B), beyond the plan: `ready → gated`; `--grade` exits 1 when any ref is unreadable.** Why: a stale cleared-date on a gated phase misreads as cleared; an unreadable ref fails closed and a scheduled run must notice. + +Choices made in phase 2 — the conductor, the board and the workflows +(PyAutoMind#380): + +38. **The dashboard normaliser strips the generated-comment line AND the + visible `Last updated ` banner, in both `dashboard.md` and + `dashboard.html`, before comparing.** Why: the Mind strips only the + comment, so its banner changes date every night and `--check` reports + drift on an unchanged registry — 348 self-heal commits in 30 days for a + page nobody had touched. A no-op night here commits nothing. +39. **`collect` scores each leg PASS / FAIL / **UNOBSERVABLE**, and a member's + `` is FAILED if any leg failed, SUSPECT if any leg is + unobservable, HEALTHY otherwise; a record's `delivered:` counts the + HEALTHY members.** Why: two of the four delivery legs cannot be seen from + the laptop today — the checkpoint is excluded from both projects' pulls, + and `subhalo_validation` writes no structured version stamp — and scoring + an unobservable leg as PASS would manufacture evidence while scoring it + FAIL would cry wolf. SUSPECT says exactly what is true: nobody looked. +40. **The pull manifest is `/.cortex/pull.json`: + `{"pulled_at": , "runs": {"": {"checkpoint_bytes": , + "checkpoint_mtime": }}}`, written by each project's own sync CLI + (phase 3), read by `collect` when present.** Why: "when was this pulled, + and did the checkpoint grow?" is knowable only at the moment of the pull; + the puller is the only process that can record it. Until the CLIs write + it, the checkpoint leg stays UNOBSERVABLE, tagged `RAL only`. +41. **`collect --pull` is opt-in and shells out to the project's own + `sync_cli pull`; nothing in the Cortex or the conductor ever submits a + job.** Why: the laptop lane is human-driven by ruling (AGENTS.md); the + conductor may refresh what is already on disk when asked, but a + submission is a spend of the machine and of the human's attention, and it + stays a typed command. +42. **The Brain conductor spells the writing flag `--apply`; `scripts/cortex.py` + spells it `--write`.** Why: `--apply` is the Brain's house spelling across + every conductor and the phase-2 prompt asked for it; `--write` is already + the Cortex script's, documented and tested. `cortex gates --grade --apply` + is a thin wrapper — one edit, two doors — and neither vocabulary is bent to + the other. +43. **`gates_grade.yml` is the only scheduled job that mutates the ledger, and + it may only move `gated → ready` (or `ready → gated` on a reopened gate).** + Why: "is issue #380 closed?" is a fact GitHub answers, not a judgement, and + a phase arriving in `ready` costs nothing until a human admits it into a + slot. Everything else on a timer reads or re-renders. It commits what + flipped **before** it fails on an unreadable ref, so a red run never means + the flips were lost. +44. **Every workflow that can push to `main` shares `concurrency: group: + cortex-main-writers, cancel-in-progress: false`.** Why: the race is not + between two merges, it is between two writers of the tip — + `ledger_merge.yml`'s merge, `dashboard_refresh.yml`'s self-heal and + `gates_grade.yml`'s flips would otherwise burn their retry loops against + each other. Queue, never cancel: a cancelled run can leave a branch merged + and undeleted. `pages_dashboard.yml` keeps GitHub's own `group: pages`. +45. **`AGENTS.md` and `TEMPLATE.md` under a ledger dir are CODE** + (`EXCLUDED_NAMES`), so `rulings/AGENTS.md`, `batches/AGENTS.md`, + `batches/packets/{AGENTS.md,TEMPLATE.md}` and `batches/reviews/AGENTS.md` + never auto-merge. Why: they are ledger by location and instructional by + content — they say what an entry may be and what every future entry is + stamped from. Auto-merging a rewrite of `rulings/AGENTS.md` would let a + branch edit the rule that governs its own merge. +46. **`dashboard.md` and `dashboard.html` are LEDGER** (`LEDGER_FILES`). Why: + they are generated from the ledger and hand-edited by nobody; a branch that + moves a phase re-renders them in the same push, and + `dashboard_refresh.yml`'s self-heal commit must land without a human. If + they were code, every ordinary ledger branch would stop for review on two + files nobody wrote. +47. **PyAutoBrain's tests for the cortex conductor depend on a real + PyAutoCortex checkout beside the Brain** (a third checkout in `tests.yml`; + the tests `pytest.skip` when it is absent). Why: the renderer's contract is + with this repo's fixtures (`tests/fixtures/skeleton`), and a copy of them + inside the Brain would drift from the schema they are the witness for. +48. **The page's GitHub home is read from the repo's own `README.md` / + `AGENTS.md` links, falling back to the git remote — never from a hard-coded + org.** Why: the render must be byte-identical on the laptop and inside + `dashboard_refresh.yml`, or `--check` would report permanent drift and the + self-heal would commit a page every night; a file that travels with the + repo is readable in both places, whereas a git remote is not guaranteed in + every CI container. A fork's Cortex renders its own owner. + +49. **Four `collect` scoring rules fixed while implementing it (2026-09-01).** + A `.zip` beside a run directory outranks the directory — seven subhalo + dirs were stale partial extractions, so `search.summary` is read through + `zipfile` when the zip exists. An `.err` that is non-empty, not all warnings + and carries no fatal marker is UNOBSERVABLE (named first unexplained line), + never PASS or FAIL — the machine cannot score it. "Newer than submitted" + means newer than the phase's *first* submission, so a resubmit does not + stale the real witness. Witness hits are ranked (run stem or `Where to look` + leaf in the path first) because `witness_file` is project-wide and phases + share one tree. Also: `submitted → pulled` is not an edge, so a `submitted` + member is scored but left with a note rather than moved. diff --git a/scripts/ledger_merge.py b/scripts/ledger_merge.py index e71a8d1..9975147 100755 --- a/scripts/ledger_merge.py +++ b/scripts/ledger_merge.py @@ -59,12 +59,30 @@ # and on purpose — and projects.yaml, which is CODE: `sync_cli` and # `local_path` are paths the batch conductor executes under, worse than the # Mind's repos.yaml, which is already on the human side of its line. -LEDGER_FILES = ("epics.md",) - -# Names that are ledger by location but executable by collection. A file -# pytest would *collect* runs in CI from anywhere in the tree, so it is code -# wherever it sits — a `test_*.py` dropped beside a batch packet included. -EXCLUDED_NAMES = ("conftest.py", "test_*.py", "*_test.py") +# +# `dashboard.md` / `dashboard.html` join it as GENERATED ledger: the cortex +# conductor renders them from the registry, `dashboard_refresh.yml` self-heals +# them on main, and a branch that moves a phase re-renders them in the same +# push. If they were code, every ordinary ledger branch would stop for a human +# on two files nobody wrote by hand. +LEDGER_FILES = ("epics.md", "dashboard.md", "dashboard.html") + +# Names that are ledger by location but must not ride along, for two different +# reasons. +# +# EXECUTABLE BY COLLECTION: a file pytest would *collect* runs in CI from +# anywhere in the tree, so it is code wherever it sits — a `test_*.py` dropped +# beside a batch packet included. +# +# INSTRUCTIONAL BY CONTENT: `AGENTS.md` and `TEMPLATE.md` under a ledger dir +# (`rulings/AGENTS.md`, `batches/AGENTS.md`, `batches/packets/AGENTS.md`, +# `batches/packets/TEMPLATE.md`, `batches/reviews/AGENTS.md`) are not entries in +# the ledger — they are the doctrine that says what an entry may be, and the +# template every future entry is stamped from. They are read by agents as +# instructions, so a change to one is a change to behaviour: it needs a human, +# exactly as `scripts/` does. Auto-merging a rewrite of `rulings/AGENTS.md` +# would let a branch edit the rule that governs its own merge. +EXCLUDED_NAMES = ("conftest.py", "test_*.py", "*_test.py", "AGENTS.md", "TEMPLATE.md") def is_ledger_path(path: str) -> bool: diff --git a/tests/test_ledger_merge.py b/tests/test_ledger_merge.py index 03015ed..b641089 100644 --- a/tests/test_ledger_merge.py +++ b/tests/test_ledger_merge.py @@ -5,9 +5,14 @@ unclassified, no traversal or dotfile route past the ledger prefixes, no pytest-collectable file smuggled in beside a packet, every code home the repo has (scripts/, tests/, .github/, policy/, docs/, projects.yaml, the prose -pages) staying on the human side of the line — and, the Cortex's own addition, -a MODIFIED, DELETED or RENAMED ruling never auto-merging (rulings/ is -append-only). +pages) staying on the human side of the line, no doctrine file (AGENTS.md, +TEMPLATE.md) riding along because it happens to sit under a ledger dir — and, +the Cortex's own addition, a MODIFIED, DELETED or RENAMED ruling never +auto-merging (rulings/ is append-only). + +The one thing this gate must NOT refuse is the generated board: `dashboard.md` +and `dashboard.html` are rendered from the ledger and self-healed on main, so +they are ledger too. """ import subprocess @@ -31,6 +36,9 @@ def test_ledger_dirs_and_registry_files_are_ledger(): "batches/reviews/2026-09-01-pm.md", "batches/packets/2026-09-01-pm.html", "epics.md", + # generated from the ledger, self-healed on main by dashboard_refresh.yml + "dashboard.md", + "dashboard.html", ): assert ledger_merge.is_ledger_path(path), path @@ -43,6 +51,9 @@ def test_every_code_home_needs_a_human(): "tests/fixtures/skeleton/phases/example/01_scope.md", ".github/workflows/ledger_merge.yml", ".github/workflows/cortex_check.yml", + ".github/workflows/dashboard_refresh.yml", + ".github/workflows/pages_dashboard.yml", + ".github/workflows/gates_grade.yml", ".gitignore", ".claude/settings.json", ".claude/hooks/session-start.sh", @@ -85,6 +96,38 @@ def test_inert_assets_ride_along_but_collectable_tests_do_not(): assert not ledger_merge.is_ledger_path(path), path +def test_doctrine_under_a_ledger_dir_needs_a_human(): + """A ledger dir holds entries; `AGENTS.md` and `TEMPLATE.md` inside one are + not entries, they are the doctrine that says what an entry may be and the + template every future entry is stamped from. A branch that could auto-merge + `rulings/AGENTS.md` could edit the rule governing its own merge.""" + for path in ( + "rulings/AGENTS.md", + "batches/AGENTS.md", + "batches/packets/AGENTS.md", + "batches/packets/TEMPLATE.md", + "batches/reviews/AGENTS.md", + # by name, not by that fixed list — a doctrine file in a dir nobody has + # created yet is code on the day it appears + "phases/example/AGENTS.md", + ): + assert not ledger_merge.is_ledger_path(path), path + # ordinary entries beside them still merge + assert ledger_merge.is_ledger_path("batches/packets/2026-09-01-pm.html") + assert ledger_merge.is_ledger_path("batches/reviews/2026-09-01-pm.md") + + +def test_every_tracked_file_under_a_ledger_dir_gets_the_right_verdict(): + """The witness on the live tree: every tracked path under phases/, rulings/ + and batches/ is ledger unless it is doctrine.""" + out = subprocess.run(["git", "ls-files", "phases", "rulings", "batches"], cwd=REPO, + capture_output=True, text=True, check=True).stdout.split() + assert out, "no ledger-dir paths tracked" + for path in out: + doctrine = Path(path).name in ("AGENTS.md", "TEMPLATE.md") + assert ledger_merge.is_ledger_path(path) is not doctrine, path + + def test_classify_splits_and_dedupes_preserving_order(): ledger, blocked = ledger_merge.classify( ["epics.md", "scripts/x.py", "epics.md", "phases/a/b.md", "", " "] @@ -226,3 +269,30 @@ def test_workflow_invokes_the_script_as_implemented(): workflow = (REPO / ".github" / "workflows" / "ledger_merge.yml").read_text() assert "python3 scripts/ledger_merge.py classify --base origin/main" in workflow assert "python3 scripts/cortex.py check" in workflow + + +def test_every_workflow_this_repo_has_is_code(): + """Enumeration, not a sample: no workflow file may ever auto-merge, and the + three added with the dashboard (two of which push to main) are named here so + the list cannot silently fall behind the directory.""" + tracked = set(subprocess.run(["git", "ls-files", ".github/workflows"], cwd=REPO, + capture_output=True, text=True, check=True).stdout.split()) + for name in ("cortex_check.yml", "ledger_merge.yml", "dashboard_refresh.yml", + "pages_dashboard.yml", "gates_grade.yml"): + assert f".github/workflows/{name}" in tracked, name + for path in tracked: + assert not ledger_merge.is_ledger_path(path), path + + +def test_the_page_workflows_spell_the_conductor_and_the_grader_as_implemented(): + """`--cortex` is a flag of the SUBCOMMAND, not a global: `dashboard --cortex + . --check` runs, `--cortex . dashboard --check` exits 2 (argparse) and the + check() wrapper would report a renderer failure. Pin the spelling.""" + refresh = (REPO / ".github" / "workflows" / "dashboard_refresh.yml").read_text() + assert 'python3 "$BRAIN" dashboard --cortex . --check' in refresh + assert 'python3 "$BRAIN" dashboard --cortex . --apply' in refresh + assert "git add dashboard.md dashboard.html" in refresh + grade = (REPO / ".github" / "workflows" / "gates_grade.yml").read_text() + assert "python3 scripts/cortex.py gates --grade --write" in grade + # the one scheduled mutator commits phase headers and nothing else + assert "git add phases" in grade