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
568 changes: 568 additions & 0 deletions .claude/hooks/session-start.sh

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
}
]
}
]
}
}
46 changes: 46 additions & 0 deletions .github/workflows/cortex_check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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.

on:
push:
branches: [main]
paths:
- "phases/**"
- "rulings/**"
- "batches/**"
- "projects.yaml"
- "epics.md"
- "scripts/**"
- "tests/**"
- ".github/workflows/cortex_check.yml"
pull_request:
paths:
- "phases/**"
- "rulings/**"
- "batches/**"
- "projects.yaml"
- "epics.md"
- "scripts/**"
- "tests/**"
- ".github/workflows/cortex_check.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: cortex check
run: python3 scripts/cortex.py check
- name: tests
run: |
python3 -m pip install --quiet pytest pyyaml
python3 -m pytest tests -q
256 changes: 256 additions & 0 deletions .github/workflows/ledger_merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
name: Cortex Ledger Merge

# Lands PyAutoCortex's own ledger work without anyone having to ask for it.
#
# THE PROBLEM. A branch-scoped session — the phone, claude.ai/code, any
# `claude/**` flow — pushes a phase move, a ruling, a batch record to a feature
# branch and it stops there: nothing downstream looks at a `claude/**` push.
# The Mind learned this first (mind_ledger_merge.yml); the Cortex is born with
# the answer.
#
# WHY A WORKFLOW AND NOT THE SESSION. A workflow's GITHUB_TOKEN is a different
# credential from the session's, and this repo trusts it with `contents:
# write`. Running the merge here means the ledger lands from any surface that
# can push — including a chat on a phone — whether or not the session that
# pushed it ever comes back.
#
# WHAT IT WILL AND WILL NOT MERGE. Only a branch whose whole diff is *ledger*:
# `phases/`, `rulings/`, `batches/`, `epics.md`. The line is drawn by
# scripts/ledger_merge.py — a script, so it is testable and a session can
# predict the verdict — and it is DEFAULT DENY: anything under `scripts/`,
# `tests/`, `.github/`, `policy/`, `docs/`, `projects.yaml`, the prose pages,
# or any path nobody has classified, stops the merge and leaves the branch for
# a human. So does a MODIFIED or DELETED file under `rulings/` — the ledger of
# record is append-only, and the classifier reads `git diff --name-status` to
# hold that line. This workflow's own file is on the code side, which is the
# intended self-consistency: it cannot merge a change to itself.
#
# THE TRIAL-MERGE CHECK. `cortex.py check` is run on the branch tip (so audit
# mode reports something) and AGAIN on the merged tree, after `git merge
# --no-ff` and before `git push`. That second run is what catches the
# ruling-id race: two branches that both assign `R-<date>-03` each pass alone
# and fail merged. A failing check resets the trial merge and leaves the
# branch for a human.

on:
push:
branches: ["claude/**"]
workflow_dispatch:
inputs:
branch:
description: "Branch to judge (default: the caller's ref)"
type: string
default: ""
mode:
description: "audit = classify and report only · merge = land it"
type: choice
options: [audit, merge]
default: audit
delete_branch:
description: "Delete the branch after a direct (non-PR) merge"
type: boolean
default: true

# contents: write → the merge push to main and the branch delete.
# pull-requests: write → merge through an open PR when the branch has one, so
# the PR records as MERGED rather than being orphaned by a direct push.
# actions: write → re-dispatch cortex_check.yml on main. A push made with
# GITHUB_TOKEN triggers nothing, so without this main's check never runs on
# the merge commit.
permissions:
contents: write
pull-requests: write
actions: write

# Every run of this workflow ends in a push to main, so runs must not overlap:
# 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.
concurrency:
group: cortex-ledger-merge
cancel-in-progress: false

jobs:
merge:
# Never in a fork: this job writes to main.
if: github.repository == 'PyAutoLabs/PyAutoCortex' && github.event.deleted != true
runs-on: ubuntu-latest
steps:
- name: Resolve the branch
id: target
run: |
BRANCH="${{ inputs.branch }}"
[ -n "$BRANCH" ] || BRANCH="${{ github.ref_name }}"
case "$BRANCH" in
main|"") echo "::error::refusing to operate on '$BRANCH'"; exit 1 ;;
archive/condemned/*) echo "::error::'$BRANCH' is a Gut transit ref"; exit 1 ;;
esac
MODE="${{ inputs.mode }}"
# A push is the automatic door and merges; the manual door defaults to
# audit, so a human dispatching it to look never merges by accident.
[ "${{ github.event_name }}" = "push" ] && MODE="merge"
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
echo "mode=$MODE" >> "$GITHUB_OUTPUT"
echo "Branch: \`$BRANCH\` · mode: \`$MODE\`" >> "$GITHUB_STEP_SUMMARY"

- uses: actions/checkout@v4
with:
ref: ${{ steps.target.outputs.branch }}
# Full history: the classifier diffs against the merge base with main,
# and `merge-base --is-ancestor` cannot answer on a shallow clone —
# it reports "not an ancestor" for ancestry that is merely absent.
fetch-depth: 0

- name: Is there anything to merge?
id: ahead
run: |
git fetch --quiet origin main
HEAD_SHA=$(git rev-parse HEAD)
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
if git merge-base --is-ancestor "$HEAD_SHA" origin/main; then
echo "already in main — nothing to merge" >> "$GITHUB_STEP_SUMMARY"
echo "merged=already" >> "$GITHUB_OUTPUT"
else
echo "merged=no" >> "$GITHUB_OUTPUT"
fi

- name: Classify the diff — ledger, or code?
id: classify
if: steps.ahead.outputs.merged == 'no'
run: |
set +e
OUT=$(python3 scripts/ledger_merge.py classify --base origin/main)
RC=$?
set -e
echo "$OUT"
{
echo ""
echo "<details><summary>Classification</summary>"
echo ""
echo '```'
echo "$OUT"
echo '```'
echo ""
echo "</details>"
} >> "$GITHUB_STEP_SUMMARY"
# Exit 2 is the gate failing to run, which is not "a human's turn" —
# it is a broken gate, and it must fail the job rather than read as a
# quiet decline to merge.
if [ "$RC" -eq 2 ]; then
echo "::error::ledger_merge.py could not classify the diff"
exit 1
fi
if [ "$RC" -ne 0 ]; then
echo "verdict=code" >> "$GITHUB_OUTPUT"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Left for a human** — this branch changes code, or edits a ruling, not just the ledger." >> "$GITHUB_STEP_SUMMARY"
else
echo "verdict=ledger" >> "$GITHUB_OUTPUT"
fi

# Structural drift on the branch tip BLOCKS. A phase in a state its
# 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.
- name: Structural drift on the branch (blocking)
if: steps.classify.outputs.verdict == 'ledger'
run: python3 scripts/cortex.py check

- name: Merge into main
id: merge
if: steps.classify.outputs.verdict == 'ledger' && steps.target.outputs.mode == 'merge'
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ steps.target.outputs.branch }}
HEAD_SHA: ${{ steps.ahead.outputs.head_sha }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

# An open PR is merged THROUGH the PR, so it records as MERGED and
# its review thread closes properly. A direct push would land the
# same commits and leave the PR reading "closed", which is a lie
# about what happened to the work. The trial-merge check still runs
# first, on a local merge of the same head, so a PR cannot bypass it.
PR=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty')

# No PR, or a PR: either way, build the merge locally first and run
# the check on the MERGED tree. Each attempt rebuilds on the current
# tip of main, so a concurrent push never needs a rebase — the same
# convergence the Mind's self-heal loops use.
for attempt in 1 2 3; do
git fetch --quiet origin main
if git merge-base --is-ancestor "$HEAD_SHA" origin/main; then
echo "landed by a concurrent run"
echo "via=concurrent" >> "$GITHUB_OUTPUT"
exit 0
fi
git checkout -B _ledger_merge origin/main
if ! git merge --no-ff "$HEAD_SHA" \
-m "cortex: auto-merge ledger branch $BRANCH" \
-m "Ledger-only diff (scripts/ledger_merge.py), cortex check clean on the merged tree." ; then
git merge --abort || true
echo "::error::'$BRANCH' conflicts with main — resolve it by hand"
echo "**Conflict** — \`$BRANCH\` does not merge cleanly; left for a human." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
# THE TRIAL-MERGE CHECK. The branch passed alone; does it pass
# merged? This is where two branches that both assigned the same
# ruling id, or both moved the same phase, are caught.
if ! python3 scripts/cortex.py check; then
git reset --hard origin/main
echo "::error::'$BRANCH' merges cleanly but 'cortex.py check' fails on the merged tree — left for a human"
echo "**Blocked** — \`$BRANCH\` fails \`cortex.py check\` once merged with main (a ruling-id or phase-state race?); left for a human." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
if [ -n "$PR" ]; then
echo "open PR #$PR on this branch — merging through it"
gh pr merge "$PR" --merge
echo "via=pr#$PR" >> "$GITHUB_OUTPUT"
echo "Merged **#$PR** (\`$BRANCH\` → main)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if git push origin HEAD:main; then
echo "merged on attempt $attempt"
echo "via=direct" >> "$GITHUB_OUTPUT"
echo "Merged \`$BRANCH\` → main (no PR; direct merge commit)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "push rejected (attempt $attempt) — retrying on the new tip of main"
done
echo "::error::could not push the merge after 3 attempts"
exit 1

# A PR merge deletes its own head where the repo has "Automatically
# delete head branches" on; a DIRECT merge has no such hook. Delete it
# here, on the same proof branch_sweep uses — main must actually contain
# the head sha — so a failed or partial merge never loses a branch.
- name: Delete the merged branch
if: steps.merge.outputs.via != '' && inputs.delete_branch != false
env:
BRANCH: ${{ steps.target.outputs.branch }}
HEAD_SHA: ${{ steps.ahead.outputs.head_sha }}
run: |
git fetch --quiet origin main
if ! git merge-base --is-ancestor "$HEAD_SHA" origin/main; then
echo "::warning::main does not contain $HEAD_SHA — keeping '$BRANCH'"
exit 0
fi
if git push origin --delete "$BRANCH"; then
echo "Deleted \`$BRANCH\`." >> "$GITHUB_STEP_SUMMARY"
else
# Already gone (a PR merge with auto-delete on) is the common case
# and is not a failure.
echo "::warning::could not delete '$BRANCH' — it may already be gone"
fi

# The merge push above was made with GITHUB_TOKEN, which triggers no
# workflows, so main's check must be asked explicitly or the merge
# commit is never checked on main.
- name: Run the check on main
if: steps.merge.outputs.via != ''
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"
Loading
Loading