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
File renamed without changes.
File renamed without changes.
20 changes: 14 additions & 6 deletions .github/workflows/heart-health.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,15 @@ jobs:
run: |
mkdir -p _site
# --cloud marks the local-only checks as "not observed here"; every
# file below is a projection of the same snapshot + verdict.
PYTHONPATH="$PWD" python -m heart.dashboard --cloud --html > _site/index.html
PYTHONPATH="$PWD" python -m heart.dashboard --cloud --badge > _site/badge.json
PYTHONPATH="$PWD" python -m heart.dashboard --cloud --md > board.md
# file below is a projection of the same snapshot + verdict. A
# committed state/devbox_board.json (pyauto-heart publish) is
# auto-detected and fills the unobserved rows, age-stamped.
PYTHONPATH="$PWD" python -m heart.dashboard --cloud --html > _site/index.html
PYTHONPATH="$PWD" python -m heart.dashboard --cloud --badge > _site/badge.json
PYTHONPATH="$PWD" python -m heart.dashboard --cloud --md > board.md
# The README carries only the brief strip; the full board lives on
# the Pages page (one-tap 📋 fix prompts) and the step summary.
PYTHONPATH="$PWD" python -m heart.dashboard --cloud --md-brief > readme_strip.md

- name: Write the board to the job step summary
run: |
Expand All @@ -115,7 +120,7 @@ jobs:
import pathlib, re
readme = pathlib.Path("README.md")
text = readme.read_text()
board = pathlib.Path("board.md").read_text().strip()
board = pathlib.Path("readme_strip.md").read_text().strip()
begin, end = "<!-- heart:begin -->", "<!-- heart:end -->"
block = f"{begin}\n{board}\n{end}"
if begin in text and end in text:
Expand Down Expand Up @@ -156,7 +161,10 @@ jobs:
for f in sorted(state.glob("*.ci_status.json")):
d = json.loads(f.read_text())
if d.get("conclusion") not in (None, "", "success"):
red.append(f"- **{d['name']}**: CI {d.get('conclusion')} ({d.get('workflow','?')})")
line = f"- **{d['name']}**: CI {d.get('conclusion')} ({d.get('workflow','?')})"
if d.get("url"):
line += f" — [failing run]({d['url']})"
red.append(line)
for f in sorted(state.glob("*.open_prs.json")):
d = json.loads(f.read_text())
age = d.get("max_age_days", 0)
Expand Down
81 changes: 42 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,56 +9,59 @@
[![health](https://img.shields.io/endpoint?url=https://pyautolabs.github.io/PyAutoHeart/badge.json)](https://pyautolabs.github.io/PyAutoHeart/)

<!-- The block below is auto-updated by .github/workflows/heart-health.yml (everything -->
<!-- between the heart:begin/heart:end markers is replaced with the rendered board). -->
<!-- between the heart:begin/heart:end markers is replaced with the rendered strip). -->
<!-- Live board: https://pyautolabs.github.io/PyAutoHeart/ -->
<!-- heart:begin -->
## 🔴 PyAuto health — **RED** (score 45)

_snapshot `2026-08-19T05:27:02.238675+00:00` · just now_

**Blockers:** autofit_workspace_test: Smoke Tests failure on main

| | Check | Status |
|--|--|--|
| 🟢 | Libraries | 6 repos nominal |
| 🔴 | Workspaces | 11 repos, 2 need attention |
| ⚪ | Worktree drift | not observed here (dev-box only) |
| ⚪ | Script timing | not observed here (dev-box only) |
| ⚪ | Import timing | not observed here (dev-box only) |
| ⚪ | Unit-test timing | not observed here (dev-box only) |
| ⚪ | Profiling drift | not observed here (dev-box only) |
| ⚪ | Workspace test-mode timing | not observed here (dev-box only) |
| ⚪ | Test run | not observed here (dev-box only) |
| ⚪ | Version skew | not observed here (dev-box only) |

[Full board](https://pyautolabs.github.io/PyAutoHeart/)
<!-- heart:end -->

The health layer of the PyAuto organism. Heart continuously watches every
repo — branch state, CI, open PRs, version skew, script timing, workspace
validation — and rolls what it sees into one authoritative verdict:
**PyAutoHeart is the Heart of the PyAutoScientist** — the organism's health
authority. It continuously watches every repository (CI, branches, open PRs,
version skew, script and test timings, workspace validation) and rolls what it
sees into one authoritative verdict: **GREEN / STALE / YELLOW / RED**. GREEN
means it is safe to release.

```bash
pyauto-heart readiness # GREEN / YELLOW / RED, a score, and the reasons
```
See the live **[health board](https://pyautolabs.github.io/PyAutoHeart/)** for
the whole picture on one page — every check a traffic-light row, and every red
or yellow finding carrying links to the failing run and a one-tap 📋 button
that copies a ready-made Claude prompt (`/bug …`), so going from "something is
red" to "an agent is fixing it" is copy → paste, on a laptop or a phone.

GREEN means it is safe to release. Heart is an observer: it never writes
into other repos and never triggers a build — the Brain reads the verdict
and decides what to do with it.
## How PyAutoHeart works

Daily driving:
1. **Observe.** `pyauto-heart tick` (or the 5-minute daemon) runs the cheap
checks — repo state, CI conclusions, open PRs, worktree drift, timings —
into one cached snapshot. Deep checks (install verification, workspace
validation, URL hygiene) run on demand or on cloud schedules.
2. **Judge.** `pyauto-heart readiness` rolls the snapshot into the verdict and
score. STALE means evidence is missing or expired, nothing known-bad — the
remedy is re-running a check, never fixing code.
3. **Show.** One renderer projects the same snapshot everywhere, so the
surfaces cannot disagree: the [Pages board](https://pyautolabs.github.io/PyAutoHeart/),
the README strip above, the badge, the terminal board, and the JSON surface
agents consume. A daily cloud run publishes the board and keeps a single
`[heart-health]` tracking issue open while anything is degraded.
4. **Enrich.** The cloud can only see API-safe signals; checks needing a
working tree are measured on the dev box, and `pyauto-heart publish` pushes
a distilled observation so the same page fills in, age-stamped.
5. **Gate.** The Heart only observes — it never edits other repos and never
triggers a build. The Brain reads the verdict (`/health`) and decides;
releases require GREEN.

## CLI examples

```bash
pyauto-heart readiness # GREEN / STALE / YELLOW / RED, a score, and the reasons
pyauto-heart tick # one refresh cycle
pyauto-heart status # pretty-print the cached state
pyauto-heart watch # the daemon: tick every 5 min, live board on a tty
pyauto-heart dashboard # the board (also --md, --html, --json, --oneline)
pyauto-heart dashboard # the board (also --md, --md-brief, --html, --json, --oneline)
pyauto-heart publish # push the dev-box observation to the live board
pyauto-heart fix ci <repo> # bundle a failing topic into a paste-ready Claude prompt
```

Runs from its checkout (`PYTHONPATH` + `PATH`, no pip install); state lives
under `~/.pyauto-heart/`. Which repos are polled, and with what thresholds,
is `config/repos.yaml`. Tests: `pytest tests/`.

Boundary and agent guidance: [AGENTS.md](AGENTS.md). The organism:
[PyAutoBrain/ORGANISM.md](https://github.com/PyAutoLabs/PyAutoBrain/blob/main/ORGANISM.md),
documented in full at <https://pyautoscientist.readthedocs.io>.
The full CLI surface, the run-from-checkout model, state layout, and verdict
semantics are in [REFERENCE.md](REFERENCE.md). How agents should operate this
repo is in [AGENTS.md](AGENTS.md). The organism this repo is the Heart of is
described once in
[PyAutoBrain/ORGANISM.md](https://github.com/PyAutoLabs/PyAutoBrain/blob/main/ORGANISM.md)
and documented in full at <https://pyautoscientist.readthedocs.io>.
140 changes: 140 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# PyAutoHeart reference

The operational detail behind [README.md](README.md): the full CLI surface, the
run model, state layout, checks, verdict semantics, and the board's surfaces.
(Absorbed the former `health_agent/capabilities.md`; the machine-readable
contract agents consume is `health_agent/capabilities.yaml`.)

## Running

Heart runs from its checkout — no pip install. `bin/pyauto-heart` resolves its
own repo root; put it on `PATH` (the PyAutoBrain installer does). State lives
under `~/.pyauto-heart/` (override with `HEART_STATE_DIR`); the checkout itself
is never written by checks (the observer rule — the one exception is
`pyauto-heart publish`, which commits Heart's OWN `state/devbox_board.json`).
Which repos are polled, and with what thresholds, is `config/repos.yaml`.
Tests: `pytest tests/`.

## CLI surface (`bin/pyauto-heart`)

| Subcommand | Purpose | Health role |
|---|---|---|
| `watch` / `live` | foreground monitor loop (live board on a tty) | runs the tick on a schedule |
| `tick` | one-shot refresh of all checks into `state.json` | produces the snapshot |
| `stop` | kill the daemon (`--all` sweeps orphans) | operational |
| `status` | coloured snapshot (`--json`, `--quiet`) | the agent's detail query |
| `readiness` | the authoritative green/stale/yellow/red verdict + score | **the gate** |
| `dashboard` | the unified board (`--oneline/--md/--md-brief/--html/--json/--badge`, `--cloud`, `--devbox`) | every surface, one renderer |
| `publish` | push the distilled dev-box board into the repo | fills the cloud page's grey rows |
| `logs` | tail the daemon log | operational |
| `fix` | emit a Claude remediation bundle (`ci`/`dirty`/`drift`/`timing`) | remediation entry point |
| `validate` | ingest release-validation artifacts into `validation_report.json` | release rehearsal evidence |
| `smoke` | isolated local workspace smoke suites | deep validation |
| `verify_install` | deep pip/conda install-path check (slow) | deep readiness signal |
| `url_check` / `url_sweep` | offline URL-hygiene guard / ecosystem sweep | monitoring only |

## The board and its surfaces

`heart/dashboard.py` is the ONE renderer: every surface is a projection of the
same `state.json` + `release_ready.json`, so they cannot disagree.

- **Pages board** — <https://pyautolabs.github.io/PyAutoHeart/>, published daily
by `heart-health.yml`. Blockers link the repo and the failing run, and carry
one-tap 📋 buttons copying a ready-made `/bug …` Claude prompt; grey
dev-box-only rows say what they watch and copy the observe command.
- **README strip** — the `heart:begin/end` block (`--md-brief`): verdict +
linked blockers + board link, auto-committed by the same workflow.
- **Badge** — `badge.json` on the Pages site, rendered via shields.io.
- **Terminal** — `pyauto-heart dashboard` / `status` / the `watch` daemon.
- **JSON** — `--json` (schema v2: structured `blockers` with prompts/links,
per-section `action`/`links`/`observed_ago`) — what the Health Agent and
mobile consume.
- **Issue** — one `[heart-health]` tracking issue opens while cloud checks are
degraded and closes when clean.

### Cloud-only honesty and the dev-box publish

The cloud job only observes API-safe checks (`ci_status`, `open_prs`); the
local-only families (`heart/dashboard.py::LOCAL_ONLY_FAMILIES` — worktree
drift, script/import/unit-test/test-mode timings, profiling drift, test run,
version skew, repo state) render "not observed here" rather than fake green.
`pyauto-heart publish` distills the dev box's board for those families into
`state/devbox_board.json` (states/summaries/counts only — detail lines naming
local filesystem paths are scrubbed) and pushes it; the cloud render merges the
file, stamping each row "observed Nh ago on the dev box" and letting it expire
back to grey after 48 h (`DEVBOX_FRESH_SECONDS`).

## Checks

**Continuous** (cheap, every `<30s` tick — `heart/tick.sh`):

- **repo_state** (`checks/repo_state.sh`) — branch / dirty (real vs generated) /
ahead / behind, per repo. RED when a library is off `main`, has uncommitted
source, or is behind origin.
- **ci_status** (`checks/ci_status.sh`) — latest CI conclusion per repo via
`gh` (the failing run's URL is cached and surfaced on the board). RED when a
library's latest conclusion is not `success`.
- **open_prs** (`checks/open_prs.sh`) — open PR count + max age. YELLOW at `>= 7d`.
- **worktree_drift** (`checks/worktree_drift.sh`) — `PyAutoLabs-wt/` dirs vs
PyAutoMind `active.md` (orphan / missing / dirty). Monitoring.
- **script_timing** (`checks/script_timing.py`) — per-script duration vs rolling
baseline (`>1.5x` slow, `>3x` regression). YELLOW.
- **test_run** (`checks/test_run.py`) — reads the workspace-validation verdict.
YELLOW when not passing / stale / unknown (workspace debt is advisory).
- **version_skew** (`checks/version_skew.py`) — each workspace's pinned version
vs the installed library. RED on AHEAD / MISMATCH / BAD; YELLOW on
BEHIND / UNKNOWN.
- **noise** (`heart/noise.py`) — splits `git status` into genuine source drift
vs regenerated-artifact noise so only real drift drives gates.

**Deep** (slow, on-demand / cloud cron, never in the tick):

- **verify_install** (`checks/verify_install.sh`) — pip, conda, and Colab
install-path checks A–F. RED if the last run has `ready==false`; STALE if it
is find-links-only, older than 14 days, or never run.
- **url_check / url_sweep / url_check_live** — offline regex guard, ecosystem
sweep, and live HTTP reachability audit. **Monitoring only — never gates
readiness.**

## Readiness verdict (`heart/readiness.py`)

`compute(snapshot)` is a pure function rolling the snapshot into one verdict:

- **RED** — library CI failing / off main / dirty / behind; version skew
AHEAD / MISMATCH / BAD; install verification `ready==false`.
- **YELLOW** — workspace validation not passing (standing debt, advisory),
script-timing regressions, stale open PRs / parked scripts, skew BEHIND.
- **STALE** — evidence missing or expired with nothing known-bad; the remedy is
re-running a check, never fixing code. Evidence whose last known result was
adverse stays yellow/red. Releases require GREEN; the dev-ship gate treats
STALE as passing (an evidence gap is organism-scope, not branch-scope).
- **GREEN** — none of the above.

`red > yellow > stale > green`. The `score` (0–100) is advisory/sortable only —
the colour is the gate. Persisted to `~/.pyauto-heart/release_ready.json`.

## GitHub workflows (`.github/workflows/`)

- **heart-health.yml** — daily cloud sweep; renders + publishes the Pages
board, badge, README strip; maintains the `[heart-health]` issue.
- **lib-tests.yml** / **smoke-tests.yml** / **docs-build.yml** — reusable
workflows the libraries and workspaces call; Heart owns the definitions.
- **workspace-smoke.yml** → **workspace-validation.yml** (workflow_call body) —
scripts + notebooks against the libraries' current `main`; the run history
`test_run` + `readiness` consume. The release rehearsal has its own entry,
**release-integrate.yml**, so a failed rehearsal never overwrites the smoke
verdict (see `docs/release_validation.md`).
- **heart-tests.yml** — Heart's own pytest suite; **url-check.yml** — weekly
URL sweep into one `[url-check]` issue.

## State (`~/.pyauto-heart/`)

`state.json` (aggregated snapshot), `release_ready.json` (the verdict),
`validation_report.json`, per-repo sidecars, rolling `timings/`,
`url_check.json`, `verify_install.json`, daemon `heart.pid`, `logs/heart.log`.

## Internals

The check framework, the `<30s` tick budget, how to add a check, and the hard
rules (observer-only, colour coding, atomic state writes):
[docs/internals.md](docs/internals.md).
33 changes: 29 additions & 4 deletions bin/pyauto-heart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ SUBCOMMAND_ORDER=(
status
readiness
dashboard
publish
logs
fix
"# Release validation (ingest-and-judge only; never dispatches a build)"
Expand All @@ -45,6 +46,7 @@ declare -A SHORT_DESC=(
[status]="Print the cached state with green/yellow/red colouring"
[readiness]="Print the release-readiness verdict (green/yellow/red + score)"
[dashboard]="The unified health board (--oneline/--md/--html/--json); reads cache, no tick"
[publish]="Push a distilled dev-box board into the repo so the cloud page fills its grey rows"
[validate]="Ingest release-validation artifacts into validation_report.json"
[smoke]="Prepare isolated local environments and run workspace smoke suites"
[logs]="Tail the daemon log"
Expand Down Expand Up @@ -208,23 +210,29 @@ cmd_readiness() {
}

help_dashboard() { cat <<EOF
pyauto-heart dashboard [--oneline|--md|--html|--json|--badge] [--cloud]
[--quiet] [--no-color] [--stale-after SECS]
pyauto-heart dashboard [--oneline|--md|--md-brief|--html|--json|--badge]
[--cloud] [--devbox PATH] [--quiet] [--no-color]
[--stale-after SECS]

The ONE unified health board — a single renderer, many formats. It folds
today's \`status\` + \`readiness\` + the release-validation report into one screen
and reads the CACHED snapshot only (no tick), so it is instant.

Formats (default is the full colour terminal board):
--oneline compact one-liner for a venv/shell prompt hook
--md GitHub-flavoured markdown (step summary / issue / README block)
--html standalone self-contained page (GitHub Pages)
--md GitHub-flavoured markdown (step summary / issue)
--md-brief the README strip: verdict + linked blockers + board link
--html standalone self-contained page (GitHub Pages) with one-tap 📋
copy buttons (Claude prompts / commands) and run links
--json the machine surface the Health Agent + mobile card consume
--badge a shields.io endpoint-badge JSON (verdict colour)

--cloud mark the local-only checks (repo_state, worktree_drift,
script_timing, test_run, version_skew) as "not observed here" —
the honest vantage of the cloud job, which has no working tree.
--devbox P merge a published dev-box board (see \`pyauto-heart publish\`) so
unobserved rows fill in, age-stamped; under --cloud the checked-in
state/devbox_board.json is auto-detected.

Every format is a projection of the same snapshot + verdict, so the surfaces
cannot disagree. See also the sourceable venv hook: heart/shell/heart_prompt.sh.
Expand All @@ -234,6 +242,23 @@ cmd_dashboard() {
exec env PYTHONPATH="$HEART_HOME" python3 -m heart.dashboard "$@"
}

help_publish() { cat <<EOF
pyauto-heart publish [--dry-run]

Distill the LOCAL board's dev-box-only families (worktree drift, timings,
test run, version skew, profiling drift) into state/devbox_board.json and
commit+push it to the Heart repo (main only). The cloud board merges the file
so its "not observed here" rows fill in, age-stamped
"observed Nh ago on the dev box". Detail lines naming local filesystem paths
never leave the machine.

--dry-run print the distilled JSON; write and push nothing
EOF
}
cmd_publish() {
exec env PYTHONPATH="$HEART_HOME" python3 -m heart.publish "$@"
}

help_validate() { cat <<EOF
pyauto-heart validate --ingest <artifacts...> [--profile P] [--testpypi-version V]
[--commit-shas FILE] [--out FILE] [--json]
Expand Down
6 changes: 4 additions & 2 deletions health_agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ adapts with no edits.
| File | What it is |
|---|---|
| [`capabilities.yaml`](./capabilities.yaml) | Machine-readable manifest of every Heart capability — the abstract-provider self-description the Brain agent reads. |
| [`capabilities.md`](./capabilities.md) | Human-readable audit of Heart's full health surface (CLI, checks, readiness, workflows, state, docs). |
| [`pyautobuild_boundary_audit.md`](./pyautobuild_boundary_audit.md) | Audit confirming no health/readiness gating logic has drifted into PyAutoHands, with the one naming nuance and a follow-up. |

The human-readable companion audit lives in [`REFERENCE.md`](../REFERENCE.md)
(it absorbed the former `capabilities.md`; the one-off PyAutoHands boundary
audit was retired — see git history).

## Quick use (from the Brain agent)

Expand Down
Loading
Loading