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
69 changes: 61 additions & 8 deletions .github/workflows/ledger_merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ permissions:
contents: write
pull-requests: write
actions: write
# issues: write β†’ a branch that cannot land is written on an issue.
issues: 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
Expand Down Expand Up @@ -195,19 +197,33 @@ jobs:
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
# Built without committing: the two generated pages are the one
# conflict every ledger branch can produce (each re-renders them),
# and a render is never a reason to strand a ledger β€” take main's
# copy, let the merged tree be re-rendered on main by
# dashboard_refresh.yml. Anything else unmerged is a human's.
if ! git merge --no-ff --no-commit "$HEAD_SHA" >/dev/null 2>&1; then
UNMERGED=$(git diff --name-only --diff-filter=U)
OTHER=$(echo "$UNMERGED" | grep -vE '^(dashboard\.md|dashboard\.html)$' || true)
if [ -n "$OTHER" ]; then
git merge --abort || true
echo "'$BRANCH' conflicts with main on: $(echo $OTHER)" > /tmp/ledger_merge_reason
echo "::error::'$BRANCH' conflicts with main beyond the generated pages β€” resolve it by hand"
echo "**Conflict** β€” \`$BRANCH\` does not merge cleanly (\`$(echo $OTHER)\`); left for a human." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
for f in $UNMERGED; do git checkout --ours -- "$f" && git add -- "$f"; done
echo "generated pages conflicted β€” took main's copy; dashboard_refresh.yml re-renders on main"
fi
git commit --quiet --no-verify \
-m "cortex: auto-merge ledger branch $BRANCH" \
-m "Ledger-only diff (scripts/ledger_merge.py), cortex check clean on the merged tree."
# THE TRIAL-MERGE CHECK. The branch passed alone; does it pass
# merged? This is where two branches that both wrote the same
# project's ledger are caught.
if ! python3 scripts/cortex.py check; then
git reset --hard origin/main
echo "'$BRANCH' merges cleanly but 'cortex.py check' fails on the merged tree (two branches wrote the same ledger?)" > /tmp/ledger_merge_reason
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 (two branches wrote the same ledger?); left for a human." >> "$GITHUB_STEP_SUMMARY"
exit 1
Expand All @@ -234,8 +250,11 @@ jobs:
# 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.
# A branch found ALREADY in main gets the same treatment: 38 of them had
# accumulated by 2026-09-17, each merged by a run whose delete never
# fired, and every later push found "nothing to merge" and left it.
- name: Delete the merged branch
if: steps.merge.outputs.via != '' && inputs.delete_branch != false
if: (steps.merge.outputs.via != '' || steps.ahead.outputs.merged == 'already') && inputs.delete_branch != false
env:
BRANCH: ${{ steps.target.outputs.branch }}
HEAD_SHA: ${{ steps.ahead.outputs.head_sha }}
Expand All @@ -253,6 +272,40 @@ jobs:
echo "::warning::could not delete '$BRANCH' β€” it may already be gone"
fi

# A branch that cannot land is written down where a human looks: one
# open issue per branch, updated on every further attempt, closed by the
# run that finally lands it. A red run nobody is subscribed to is how
# the Mind lost six completion records (2026-09-17).
- name: Leave a note a human will see
if: failure() && steps.ahead.outputs.merged == 'no'
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ steps.target.outputs.branch }}
run: |
REASON=$(cat /tmp/ledger_merge_reason 2>/dev/null || echo "a step failed β€” see the run log")
RUN="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
TITLE="ledger merge: \`$BRANCH\` needs a human"
BODY=$(printf '%s\n\nRun: %s\nBranch: https://github.com/%s/tree/%s\n\nMerge it by hand and push to main; this issue closes itself when the branch lands.' \
"$REASON" "$RUN" "${{ github.repository }}" "$BRANCH")
gh label create ledger-merge --color d93f0b --description "a ledger branch the auto-merge could not land" --force >/dev/null 2>&1 || true
N=$(gh issue list --state open --label ledger-merge --search "in:title \"$BRANCH\"" --json number --jq '.[0].number // empty')
if [ -n "$N" ]; then
gh issue comment "$N" --body "$BODY"
else
gh issue create --title "$TITLE" --label ledger-merge --body "$BODY"
fi

- name: Close the note once the branch has landed
if: steps.merge.outputs.via != '' || steps.ahead.outputs.merged == 'already'
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ steps.target.outputs.branch }}
run: |
for N in $(gh issue list --state open --label ledger-merge --search "in:title \"$BRANCH\"" --json number --jq '.[].number'); do
gh issue close "$N" --comment "Landed: \`$BRANCH\` is in main." --reason completed
done

# 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.
Expand Down
75 changes: 28 additions & 47 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ not re-derive them.

- **`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. `cortex.py` reads
it with PyYAML and validates the fields (REFERENCE.md "projects.yaml").
execute under, so a change to it is a human's turn β€” with one exception, a
**birth**: a diff that only adds rows, each with its `projects/<key>.md` in
the same push, is ledger and lands by itself (three birth PRs sat six days
before the gate learned the difference, 2026-09-17). Editing or dropping a
row is still code. `cortex.py` reads it with PyYAML and validates the
fields (REFERENCE.md "projects.yaml").
- **`projects/<key>.md`** β€” one ledger per project (REFERENCE.md "The
ledger file"). Title `# <key> β€” <one-line summary>`; header `Project:` and
`Issue:` (`Repo#N`, an issue URL, or `none`); then exactly three sections:
Expand Down Expand Up @@ -113,13 +117,16 @@ not re-derive them.
The Cortex holds state and checks itself; it does not reason. The reasoning
lives in the Brain's **cortex conductor** β€” `pyauto-brain cortex <verb>`, or
`python3 PyAutoBrain/agents/conductors/cortex/_cortex.py <verb> --cortex
<checkout>` with no Brain install. It is read-mostly: it pulls, it shows, it
renders. The only bytes it writes of its own are the two generated pages and
<checkout>` with no Brain install. It is read-mostly, and it splits where the
machines split: `pull` shows what the cluster says (the laptop only β€” every
`local_path` is a laptop path), `checkin` stamps, renders and pushes anywhere.
The only bytes it writes of its own are the two generated pages and
`checkin.yaml`; every change to a *ledger* goes through `scripts/cortex.py`.

| Verb | What it does |
|------|--------------|
| `checkin [--dry-run \| --apply] [--push \| --no-push] [--project KEY] [--skip-pull]` | **the check-in** β€” the one door: pull every active project through its own `sync_cli`, run its `jobs` verb where it has one and show the output verbatim, re-render the board, push the ledger where the rule allows, and summarise **by project** (Now, runs, the last five entries, the commands you are likely to type next). `--dry-run` is the default and reaches nothing |
| `pull [--project KEY] [--dry-run]` | **the laptop leg** β€” pull every active project through its own `sync_cli`, then run its `jobs` verb where it has one and show the output verbatim. It writes nothing. It runs on the laptop only: a project's `local_path` exists on one machine, so where none is present it exits **2**, names each missing root and says "this is not the laptop β€” run `checkin` here and `pull` on the laptop"; where some are present it pulls those and lists the rest as skipped. `--dry-run` reaches nothing |
| `checkin [--dry-run \| --apply] [--push \| --no-push] [--project KEY]` | **the check-in** β€” the one door, on any surface: stamp `checkin.yaml`, re-render the board, push the ledger where the rule allows, and summarise **by project** (Now, the runs as the ledger holds them, the last five entries, the commands you are likely to type next). It shells out to no sync CLI. `--dry-run` is the default and writes nothing; `--skip-pull` is accepted and ignored with a notice |
| `census [--json]` | what the Cortex is holding, by project β€” 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 |
| `issue [--project KEY] [--apply]` | the concise ledger block for each project's GitHub issue; `--apply` writes it into the issue body between the markers (needs `gh`) |
Expand All @@ -133,9 +140,10 @@ human's ask β€” by the human, or by the agent in the session β€” with the
project's own `sync_cli submit` line, followed at once by `cortex.py run
<project> <jobid> "<what>"`. `running` and `done` may be typed by the agent
from what `jobs` printed, because those are cluster facts; `log --kind result`
and `--kind lesson` are typed only for words the human said. `checkin` is the
and `--kind lesson` are typed only for words the human said. `pull` is the
only leg that shells out, and only to the project's own CLI β€” the conductor
adds no SSH of its own.
adds no SSH of its own. `checkin` reaches no cluster at all, which is why it
runs on any surface while `pull` needs the laptop the science lives on.

### What runs by itself

Expand Down Expand Up @@ -205,48 +213,21 @@ Every Cortex project therefore runs from the laptop.
<!-- repos_sync:remote:begin -->
## Remote sessions (Claude Code on web and mobile)

Three facts, all measured in a web/mobile container, where this file is loaded
and little else is. They ride in every organ because a session may hold any
subset of them β€” and the session that needs this most is the one holding
several, which is exactly the session no hook fires in.
Three facts, measured in a web/mobile container. A session holding several
organs is cwd'd at the repos' *parent*, where no project hook fires, so
nothing has set it up β€” this block is what does.

- **Bootstrap in the first turn, unconditionally** β€” before the first test
command, not as a remedy once something looks wrong:

```
bash PyAutoMind/scripts/session_bootstrap.sh # ~10s cold, ~1s warm
bash PyAutoMind/scripts/session_bootstrap.sh --check # report only
```

A session holding several organs registers no SessionStart hook β€” Claude Code
reads project hooks from the project directory, which in that layout is the
repos' *parent*, not a repo β€” so nothing has set this session up. It was once
phrased as a remedy keyed to `No module named pytest` or collection
`ImportError`s naming `yaml`; that symptom stopped appearing when the
container image moved to Python 3.12, while the environment is still wrong in
ways that read like a bad command rather than a stale session (`pytest -n
auto` β†’ `unrecognized arguments: -n`). The bootstrap also **unshallows the
clones**: a remote session clones shallow, and `git merge-base --is-ancestor`
then answers "not an ancestor" for a commit whose ancestry is merely absent β€”
the answer the ship and close-out procedures act on when proving a branch
merged.

- **Then run the suite in parallel.** 4 cores, subprocess-heavy suites, no
single slow test: about 3.5x. `python3 -m pytest -q -n auto`, with
`pytest-xdist` supplied by the bootstrap above.

- **There is no `gh`, and installing one does not help.** A remote session
reaches GitHub through the `mcp__github__*` tools, already scoped to the
session's repos. `gh` installs in two seconds and is a trap: it authenticates,
then 403s every repo-scoped call, because the egress proxy serves neither the
REST repo paths nor GraphQL beyond a pinned set of PR-review operations β€” a
binary that looks healthy and fails everything that matters. It also defeats
the surface probe, which keys off `gh auth status`. Read
`PyAutoBrain/skills/GITHUB_ACCESS.md` at the top of any run that touches
GitHub; it maps each `gh` operation onto its MCP tool. Spell that path from
the workspace root, as written: a multi-organ session is cwd'd at the repos'
*parent*, so a bare `skills/…` reads as a missing file rather than a missing
repo prefix.
command, not as a remedy: `bash PyAutoMind/scripts/session_bootstrap.sh`
(`--check` reports only). It supplies pytest/PyYAML/xdist and **unshallows
the clones**, without which `git merge-base --is-ancestor` calls a merged
branch "not an ancestor" and the close-out acts on it.
- **Run the suite in parallel**: `python3 -m pytest -q -n auto` (4 cores,
~3.5x).
- **There is no `gh`, and installing one does not help** β€” it authenticates,
then 403s every repo-scoped call through the egress proxy. GitHub is the
`mcp__github__*` tools; `PyAutoBrain/skills/GITHUB_ACCESS.md` maps each
`gh` operation onto its tool and is the one full page on the subject.
<!-- repos_sync:remote:end -->
## When in doubt

Expand Down
Loading
Loading