From e33c5ec1e63efa5360162bd9be1fd9817232e2e3 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 11:50:06 +0100 Subject: [PATCH] Add manifest_drift tick check: continuous body-map identity drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs PyAutoMind/scripts/repos_sync.py --check every tick and parses its report — Heart's repos.yaml, pre_build.sh, ensure_workspace_labels.sh and every checkout's origin vs the PyAutoMind/repos.yaml body map. The drift logic stays in repos_sync.py (Mind owns the manifest; Heart observes). Classification: YELLOW per drifted surface (identity hygiene, never RED); skipped (available:false) when no PyAutoMind checkout, and an unparseable report surfaces as unavailable rather than hollow green. Wired into tick.sh, state.aggregate, readiness, and capabilities.yaml; ~1s live, 210 tests pass (8 new). Co-Authored-By: Claude Fable 5 --- health_agent/capabilities.yaml | 5 ++ heart/checks/manifest_drift.py | 117 +++++++++++++++++++++++++++++++++ heart/readiness.py | 11 ++++ heart/state.py | 1 + heart/tick.sh | 8 +++ tests/test_manifest_drift.py | 84 +++++++++++++++++++++++ tests/test_readiness.py | 21 ++++++ 7 files changed, 247 insertions(+) create mode 100644 heart/checks/manifest_drift.py create mode 100644 tests/test_manifest_drift.py diff --git a/health_agent/capabilities.yaml b/health_agent/capabilities.yaml index c53456a..00591ed 100644 --- a/health_agent/capabilities.yaml +++ b/health_agent/capabilities.yaml @@ -77,6 +77,11 @@ continuous_checks: impl: heart/checks/version_skew.py measures: "each workspace's pinned version vs the installed library" gate_role: "RED if AHEAD / MISMATCH (general.yaml vs version.txt) / BAD; YELLOW if BEHIND / UNKNOWN" + - id: manifest_drift + impl: heart/checks/manifest_drift.py + measures: "repo identity (name/owner) in Heart/Build/labels lists and checkout origins vs the PyAutoMind/repos.yaml body map" + consumes: "PyAutoMind/scripts/repos_sync.py --check (Mind owns the manifest and the drift logic; Heart only runs and reports it)" + gate_role: "YELLOW per drifted surface (identity hygiene, never RED); skipped when no PyAutoMind checkout" - id: noise impl: heart/noise.py measures: "classify git porcelain into real source drift vs regenerated-artifact noise" diff --git a/heart/checks/manifest_drift.py b/heart/checks/manifest_drift.py new file mode 100644 index 0000000..d247ede --- /dev/null +++ b/heart/checks/manifest_drift.py @@ -0,0 +1,117 @@ +"""heart/checks/manifest_drift.py — body-map identity drift. + +``PyAutoMind/repos.yaml`` is the single source of repo *identity* (GitHub +home, category, role). ``PyAutoMind/scripts/repos_sync.py --check`` verifies +the other repo lists against it — Heart's ``config/repos.yaml``, PyAutoBuild's +``pre_build.sh``, admin_jammy's ``ensure_workspace_labels.sh``, and the actual +``origin`` remote of every local checkout. That check only fires when someone +remembers to run it; this module makes it continuous by running it every tick +and parsing its report. + +The drift logic itself stays in ``repos_sync.py`` (never duplicated here — +Heart observes, Mind owns the manifest). Cheap: local file parses plus one +``git remote get-url`` per checkout, well inside the tick budget. + +Classification: drift is **YELLOW** — identity hygiene that will eventually +break a `gh` call or a label sweep, not an immediate release blocker (same +monitoring-not-gating stance as URL hygiene, but visible in readiness +cautions). A missing PyAutoMind checkout or manifest skips the check +(``available: false``) — normal for web/CI environments. + +The result lands at ``$HEART_STATE_DIR/manifest_drift.json``. +""" + +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +from pathlib import Path +from typing import Any + +HEART_HOME = Path(__file__).resolve().parents[2] +_p3 = Path(__file__).resolve().parents[3] +PYAUTO_ROOT = Path( + os.environ.get("PYAUTO_ROOT") + or (_p3 if _p3.name == "PyAutoLabs" else Path.home() / "Code" / "PyAutoLabs") +) +HEART_STATE_DIR = Path( + os.environ.get("HEART_STATE_DIR") + or Path.home() / ".pyauto-heart" +) + +_CHECK_LINE = re.compile(r"^check (?P