Skip to content

fix(hygiene): derive the conductor's repo arrays from the body map #197

Description

@Jammy2211

Overview

agents/conductors/hygiene/hygiene.sh hardcodes the repos it scans:

LIB_REPOS=(PyAutoNerves PyAutoFit PyAutoArray PyAutoGalaxy PyAutoLens)
ORG_REPOS=(PyAutoBrain PyAutoHands PyAutoHeart PyAutoMind)
DOC_REPOS=(PyAutoFit PyAutoGalaxy PyAutoLens)

PyAutoMind/repos.yaml — the body map, the single source of repo identity — declares six libraries and seven organs. So the conductor silently skips the CTI and Reduce libraries, classes PyAutoNerves as a library where the manifest calls it an organ, and covers four of seven organs. Every clean bill of health it has issued understates reality.

This is internal inconsistency, not a scope decision: the sibling scanners _hygiene_config.py and _hygiene_refs.py already reach PyAutoCTI. And repos_sync.py cannot catch it — its tenant-firewall entry for hygiene.sh (line 525) pins the drifted set as an allowlist rather than checking coverage against the manifest.

Measured, not asserted

Reproduced at PYAUTO_ROOT=/home/user before planning:

reported true
crlf cosmetic .py count 5 127 (122 in PyAutoCTI alone)
deps pyproject.toml audited 5 6
tidy managed checkouts scanned 9 ~17
$ hygiene.sh crlf
  crlf      clean     0 executable scripts w/ CRLF ...; 5 library .py w/ CRLF (cosmetic ...)
$ git -C PyAutoCTI grep -Il $'\r$' -- '*.py' | wc -l
122

A related third defect ships with this fix: when the scan root is empty or absent, every array-driven mode currently reports clean with no warning. A clean verdict over zero repos is the same failure as a clean verdict over five of eleven — it needs a distinct unscanned signal.

Scope: the coverage repair only. Widening the scan surfaces a large backlog of genuine new findings (CRLF, artifacts, dep caps). Triaging that backlog is a separate task and no finding is fixed here.

Plan

  • Add a stdlib body-map reader to the hygiene conductor that returns repo names by category from PyAutoMind/repos.yaml (PyYAML when importable, minimal parser otherwise — the conductor must not gain a hard dependency).
  • Replace LIB_REPOS / ORG_REPOS / DOC_REPOS in hygiene.sh with sets derived from that reader; no repo name is written in the script any more.
  • Key each mode off what it actually needs — deps/packaging off "ships a pyproject.toml", docs off "has docs/api/" — instead of a hand-kept array.
  • Make an empty or absent scan root, and an unreachable body map, report unscanned with a named reason instead of clean.
  • Add a coverage check to repos_sync.py that fails if the derived sets stop matching repos.yaml or if a repo name is re-hardcoded into a *_REPOS=(…) array, and shrink the now-obsolete firewall allowlist entry.
  • Cover both in tests; update the hygiene AGENTS.md mode table.
Detailed implementation plan

Work Classification

Library (organ code, no workspace leg).

Affected Repositories

  • PyAutoBrain (primary) — the conductor fix
  • PyAutoMind — the repos_sync.py coverage check

Branch Survey

Repository Current Branch Dirty?
./PyAutoBrain claude/hygiene-coverage-drift-kso7h1 clean
./PyAutoMind claude/hygiene-coverage-drift-kso7h1 clean

Branch: claude/hygiene-coverage-drift-kso7h1 (mandated by the session; overrides the usual feature/<task-name>).

Worktree root: none — cloud session, no ~/Code/PyAutoLabs-wt; work happens in the canonical checkouts.

Implementation Steps

  1. New PyAutoBrain/agents/conductors/hygiene/_hygiene_repos.py--root <dir> --category <library|organ|workspace> [--json]. Resolves the body map via PYAUTO_MIND<brain-parent>/PyAutoMind$PYAUTO_ROOT/PyAutoMind, mirroring resolve_mind in agents/_common.sh. yaml.safe_load when PyYAML imports, else a two-level indent walk over repos:<Name>:category:. Contains zero repo-name literals, so it is firewall-clean by construction. Exit 3 + empty output when the map is unresolvable, so bash can tell "none declared" from "none present".

  2. hygiene.sh — delete lines 64–66; populate LIB_REPOS / ORG_REPOS / WS_REPOS via mapfile from the helper and set BODY_MAP_OK. Then:

    • CODE_REPOS = LIB+ORG → prescan_tidy, enumerate_condemn_candidates
    • SCAN_REPOS = LIB+ORG+WS → prescan_crlf, prescan_artifacts (replaces the hardcoded workspace triple; category workspace adds autocti_workspace)
    • prescan_deps / prescan_packagingCODE_REPOS filtered on pyproject.toml present, not on category — a straight category mapping would drop PyAutoNerves (organ, but ships a distribution) and re-create this bug in a new place
    • prescan_docsCODE_REPOS filtered on docs/api/; ${#DOC_REPOS[@]} in the summary becomes the count actually scanned
  3. hygiene.sh unscanned signal — new managed_present() counts body-map repos with a .git under $ROOT. emit_json_row gains "scanned": N; N == 0status: "unscanned", count: null. Human render_row prints tag unscanned; the default scan leads with a banner naming $ROOT and the reason (scan root empty/absent vs body map unavailable), and "Recommended next" says so rather than claiming no findings.

  4. PyAutoMind/scripts/repos_sync.py — new check_hygiene_coverage(root, repos) registered in main()'s checks dict:

    • leg A — invoke _hygiene_repos.py --json; assert derived library/organ/workspace sets equal the manifest's, reporting each missing/extra name
    • leg B — regex ^\s*[A-Z_]*REPOS=\(([^)]*)\) over hygiene.sh; any manifest repo name inside is drift ("re-hardcoded — derive from the body map")
    • skips cleanly when PyAutoBrain is not checked out, matching check_heart / check_labels
    • trim FIREWALL_ALLOWLIST["…/hygiene.sh"] to the tokens that genuinely survive, verified by running --check rather than assumed
  5. TestsPyAutoBrain/tests/test_hygiene_conductor.py: derived sets match repos.yaml; PyAutoCTI/PyAutoReduce reach the crlf scan; empty root ⇒ unscanned across array modes + banner; unresolvable body map ⇒ unscanned, exit 0, valid JSON. New PyAutoMind/scripts/test_repos_sync_coverage.py (matching the existing test_spawn_*.py convention): passes on the real tree, fails on a re-hardcoded array and on a dropped category.

  6. Docs — hygiene AGENTS.md mode table: "the 3 doc repos" and the library-only framing become the derived rule; add the unscanned status alongside debris/finding/timing/surface/advisory.

Key Files

  • PyAutoBrain/agents/conductors/hygiene/hygiene.sh — lines 64–66 (the arrays), prescan_*, emit_json_row, render_row
  • PyAutoBrain/agents/conductors/hygiene/_hygiene_repos.py — new body-map reader
  • PyAutoBrain/agents/conductors/hygiene/AGENTS.md — mode table + kinds
  • PyAutoMind/scripts/repos_sync.pycheck_hygiene_coverage, FIREWALL_ALLOWLIST line 525
  • PyAutoMind/repos.yaml — read-only source of truth; unchanged

Testing

pytest PyAutoBrain/tests/test_hygiene_conductor.py; python3 PyAutoMind/scripts/repos_sync.py --check must exit 0; before/after hygiene crlf and hygiene --json under PYAUTO_ROOT=/home/user demonstrating 5 → 167 and the unscanned path under an empty root.

Trade-off accepted

Deriving the workspace set from category workspace raises the cosmetic CRLF count from 127 to 167 (autocti_workspace +40). Same class of gap as PyAutoCTI, and the backlog is deferred by design — recorded here because it is a number that will visibly jump.

Explicitly out of scope

Fixing any CRLF / artifact / dep-cap finding the widening exposes; the sibling helpers' own LIBRARIES lists in _hygiene_config.py and _hygiene_refs.py (they already cover CTI, so they are not drifted).

Original Prompt

Click to expand starting prompt
# Hygiene under-reports debt by 25x because its repo arrays skip

Type: bug
Target: PyAutoBrain
Repos:
- PyAutoBrain
Difficulty: medium
Autonomy: supervised
Priority: normal
Status: formalised

Hygiene under-reports debt by 25x because its repo arrays skip two libraries. The hygiene conductor scans a hardcoded list of checkouts in PyAutoBrain agents/conductors/hygiene/hygiene.sh. That list is stale: the LIB_REPOS array holds five entries where the body map (repos.yaml) has six, silently skipping the CTI and Reduce libraries, and it mislabels the config layer as a library; ORG_REPOS covers four of seven organs. The result is wrong output, not stale prose. On a real run the crlf mode printed '5 library .py w/ CRLF' when the true count is 127 — 122 of them in the skipped CTI library, breaking that repo's LF-only rule with nobody watching. The deps mode audits five pyproject.toml instead of six; tidy inspects nine of roughly seventeen managed checkouts. Every clean bill of health the conductor has issued understates reality. This is an internal inconsistency, since the sibling scanners _hygiene_config.py and _hygiene_refs.py already reach the CTI library. The drift checker cannot catch the gap: its tenant-firewall entry for hygiene.sh pins the current broken set as an allowlist instead of verifying coverage. The repair should derive the arrays from the body map rather than re-hardcoding them. Widening coverage will surface a large backlog of genuine new findings, so land the coverage repair and the triage of what it uncovers as separate tasks.

<!-- formalised by the Intake (Conception) Agent on 2026-08-05 from user-intake -->

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions