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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/cortex_check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -15,6 +18,8 @@ on:
- "batches/**"
- "projects.yaml"
- "epics.md"
- "dashboard.md"
- "dashboard.html"
- "scripts/**"
- "tests/**"
- ".github/workflows/cortex_check.yml"
Expand All @@ -25,6 +30,8 @@ on:
- "batches/**"
- "projects.yaml"
- "epics.md"
- "dashboard.md"
- "dashboard.html"
- "scripts/**"
- "tests/**"
- ".github/workflows/cortex_check.yml"
Expand Down
157 changes: 157 additions & 0 deletions .github/workflows/dashboard_refresh.yml
Original file line number Diff line number Diff line change
@@ -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
114 changes: 114 additions & 0 deletions .github/workflows/gates_grade.yml
Original file line number Diff line number Diff line change
@@ -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 }}
7 changes: 6 additions & 1 deletion .github/workflows/ledger_merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/pages_dashboard.yml
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading