Skip to content

Commit 4e4258d

Browse files
Jammy2211claude
andauthored
feature(pyautogut): Gut organ in ORGANISM.md + hygiene→PyAutoGut drive seam (#104)
* feature(pyautogut): add the Gut organ to ORGANISM.md + regenerate the map Register the Gut (PyAutoGut) in the canonical organ table (ORGANISM.md) and regenerate the always-loaded organism-map block in AGENTS.md via repos_sync, so every session sees the six organs. Body-map source is PyAutoMind/repos.yaml. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FB9NRvwNjXfqo9HwqsPaKP * feature(hygiene): add the hygiene → PyAutoGut drive seam (tidy/sweep) Implements feature/pyautobrain/hygiene_pyautogut_drive_seam.md — the second build follow-up of the PyAutoGut organ decision. Mirrors the Heart <-> vitals template: the organ holds and voids; the hygiene conductor decides what to condemn and when to sweep, and stays a planner (never mutates a repo). - tidy is now PyAutoGut-aware: it enumerates 95%-sure git debris (stale branches with merged-status, stashes) and emits an ASYNC condemnation plan — proposed condemned.md entries + the exact `pyauto-gut archive` calls, with a computed sweep-after (HYGIENE_CONDEMN_TRANSIT_DAYS, default 30). No synchronous per-item repo_cleanup interrogation. Merged branches skip the pen. - sweep (new mode): reads PyAutoMind/condemned.md via the stdlib _hygiene_condemned.py parser, classifies entries by their transit clock (due/pending/undated), and emits the batch void plan (`pyauto-gut void` per past-due entry) behind the existing repo_cleanup gates — no second gate. - AGENTS.md: documents both modes, the "vs Gut" boundary, and a capability audit of what the seam drives (pyauto-gut verbs, condemned.md). - Tests: tidy async-condemn contract + sweep transit-clock classification (incl. HTML-commented example exclusion); the generic per-mode delegate loop now excludes tidy (an action mode). 14 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FB9NRvwNjXfqo9HwqsPaKP --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 52cbe53 commit 4e4258d

6 files changed

Lines changed: 310 additions & 6 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Canonical boundaries live in `PyAutoBrain/ORGANISM.md`; the full body map
2121
| **Hands** | PyAutoBuild | Packaging, tagging, notebook generation, PyPI release execution. |
2222
| **Heart** | PyAutoHeart | Health/readiness — the authoritative "is it safe to release?" verdict. |
2323
| **Memory** | PyAutoMemory | Long-term scientific/software/project knowledge (see science pointer below). |
24+
| **Gut** | PyAutoGut | Owns the lifecycle of condemned self-material (stale branches, stashes, dead code/tests): holds it as durable, recoverable git refs through a transit window and voids it on a sweep. The storage mirror of Memory (retention vs release). |
2425

2526
Call chain (always this order): **Brain → Heart (gate) → Build (execute)**. Brain agents are **conductors** (front-door; a human drives them; they decide *and* act) or **faculties** (read-only opinions the conductors consult; they judge and stop). New capability grows as a faculty, not a new organ, unless it owns state or effects no existing organ can.
2627

ORGANISM.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ organs plan, build, test and release it, and you make every judgment call.
1717
| **Heart** | PyAutoHeart | Determines whether the organism is healthy. `pyauto-heart readiness` is the **authoritative** GREEN/YELLOW/RED "is it safe to release?" gate. An observer: never writes into other repos, never triggers Build. |
1818
| **Hands** | PyAutoBuild | Builds and releases — packaging, tagging, notebook generation, PyPI via `release.yml`. A pure executor: runs no readiness checks and never re-derives a gate decision. |
1919
| **Memory** | PyAutoMemory | Long-term knowledge — *what the science says* (literature wikis, concepts, bibliographies). Operational history — *what the organism did* — lives in Mind (`complete.md`, issues), not here. |
20+
| **Gut** | PyAutoGut | Owns the lifecycle of *condemned self-material* — stale branches, stashes, dead code/tests. Holds each as a durable, recoverable git ref through a transit window and **voids** it on a sweep. The storage mirror of Memory (retention ↔ release); the hygiene conductor drives it, as vitals reads Heart. |
2021

2122
The scientific libraries (PyAutoConf, PyAutoFit, PyAutoArray, PyAutoGalaxy,
2223
PyAutoLens) and the workspaces are **capabilities the organism uses, not

agents/conductors/hygiene/AGENTS.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ kinds, which is what makes its count comparable (or not):
3333
| Mode | Pre-scan (kind) | Delegates to |
3434
|------|-----------------|--------------|
3535
| `perf` | dev-loop timing — prefers Heart's tracked timing legs when present (`import_time`, `unit_test_timing`, `workspace_testmode_timing`), else times `import <pkg>` per library in a **subprocess** (**timing**) | `/refactor` / `/bug` (+ Heart timing legs) |
36-
| `tidy` | git debris — stale branches, stashes, `[gone]` refs, dirty checkouts (**debris**) | `/repo_cleanup` (Brain) |
36+
| `tidy` | git debris — stale branches, stashes, `[gone]` refs, dirty checkouts (**debris**) | **condemn** → files candidates into `condemned.md` async (PyAutoGut archives the fragile forms); no synchronous per-item gate |
37+
| `sweep` | reads `condemned.md`, classifies entries by their transit clock (**due** / pending / undated) | `pyauto-gut void` for past-due entries, behind the existing `repo_cleanup` safety gates |
3738
| `noise` | none — needs a pytest + workspace-script run (**advisory**) | `/cli_noise_clean` (Heart) |
3839
| `deps` | capped/pinned specifiers in library `pyproject.toml` (**surface**) | `/dep_audit` (Heart, hits PyPI) |
3940
| `docs` | `docs/api/*.rst` + `currentmodule` counts across the 3 doc repos (**surface**) | `/audit_docs` (Heart, imports) |
@@ -46,7 +47,8 @@ kinds, which is what makes its count comparable (or not):
4647
pyauto-brain hygiene # pre-scan across modes → ranked worklist
4748
pyauto-brain hygiene perf # import cost (subprocess) → /refactor + Heart legs
4849
pyauto-brain hygiene perf --profile <script> # cProfile a normal-mode run → rank NON-likelihood hotspots → /refactor
49-
pyauto-brain hygiene tidy # git debris → /repo_cleanup
50+
pyauto-brain hygiene tidy # git debris → condemn into condemned.md (async, no per-item gate)
51+
pyauto-brain hygiene sweep # void condemned.md entries past sweep-after → pyauto-gut void (repo_cleanup gates)
5052
pyauto-brain hygiene noise # CLI noise → /cli_noise_clean
5153
pyauto-brain hygiene deps # dependency-cap surface → /dep_audit
5254
pyauto-brain hygiene docs # API-docs surface → /audit_docs
@@ -112,3 +114,26 @@ on-demand only (never the default scan, never a Heart tick).
112114
- **vs bug / refactor** — hygiene finds and prioritises debt and *delegates* the
113115
fix to them; they own the repair.
114116
- **vs build** — hygiene is upkeep, not release; it never touches PyAutoBuild.
117+
- **vs Gut (PyAutoGut)** — the `tidy`/`sweep` modes are the **hygiene → PyAutoGut
118+
drive seam**, mirroring the **Heart ↔ vitals** template: the organ *holds and
119+
voids*, this conductor *decides what to condemn and when to sweep* and owns none
120+
of the storage. `tidy` enumerates 95%-sure debris and emits an async
121+
condemnation plan (proposed `condemned.md` entries + the exact `pyauto-gut
122+
archive` calls) — no synchronous per-item interrogation; `sweep` reads the
123+
manifest and emits the batch void plan for entries past their `sweep-after`
124+
transit date. The conductor stays a **planner** (never mutates a repo): it emits
125+
the plan, `pyauto-gut` archives/voids, and the existing `repo_cleanup` safety
126+
gates guard the sweep — there is no second gate. Design:
127+
PyAutoMind `research/pyautobrain/pyautogut_organ_decision.md`.
128+
129+
## Capability audit — what `tidy`/`sweep` drive
130+
131+
- **PyAutoGut** (`bin/pyauto-gut`): `archive <local-ref> <slug>` (materialise a
132+
fragile form under `refs/heads/archive/condemned/<slug>` before deletion),
133+
`recover <slug>` (reabsorb during transit), `void <slug>` (eliminate on sweep),
134+
`list`. Resolved via `PYAUTO_GUT` or PATH; the conductor names the commands in
135+
its plan, it does not run them.
136+
- **PyAutoMind `condemned.md`**: the catalog (symmetric to `parked.md`). `tidy`
137+
proposes entries; `sweep` reads them. Located via `resolve_mind` (`PYAUTO_MIND`
138+
`$PYAUTO_ROOT/PyAutoMind`). `HYGIENE_CONDEMN_TRANSIT_DAYS` (default 30) sets
139+
the default transit window `tidy` writes into `sweep-after`.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/env python3
2+
"""Read PyAutoMind/condemned.md for the hygiene conductor's `sweep` mode.
3+
4+
The hygiene conductor drives PyAutoGut but owns none of the storage: it reads
5+
the `condemned.md` manifest (the Mind catalog of condemned self-material) and
6+
classifies each entry by its transit clock — **due** (sweep-after date reached,
7+
ready to void) vs **pending** (still in the transit window, recoverable). It
8+
emits a plan; the actual void is delegated to `pyauto-gut void`. Stdlib only —
9+
the conductor never drags a dependency into the Brain.
10+
11+
Usage:
12+
_hygiene_condemned.py --manifest <path/to/condemned.md> [--json]
13+
14+
Exit 0 always (an absent/empty manifest is "nothing condemned", not an error).
15+
"""
16+
17+
import argparse
18+
import datetime
19+
import json
20+
import re
21+
import sys
22+
23+
# One "## <name>" block per condemned item; "- key: value" fields beneath it.
24+
# HTML-commented blocks (the schema's illustrative example) are stripped first
25+
# so they never read as live condemnations.
26+
COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
27+
HEADING_RE = re.compile(r"^##\s+(?!#)(.+?)\s*$") # '## name' (not '###')
28+
FIELD_RE = re.compile(r"^-\s*([A-Za-z][\w-]*)\s*:\s*(.*?)\s*$")
29+
30+
FIELDS = ("type", "locator", "confidence", "reason", "merged",
31+
"condemned", "sweep-after", "breaks-if-wrong", "archive-ref")
32+
33+
34+
def parse_manifest(text):
35+
"""Return a list of entry dicts. Only '##' blocks that carry at least a
36+
`type` or `locator` field count as entries (the file's prose '##' sections
37+
— Lifecycle, Entry schema — carry neither)."""
38+
text = COMMENT_RE.sub("", text)
39+
entries, cur = [], None
40+
for line in text.splitlines():
41+
h = HEADING_RE.match(line)
42+
if h:
43+
if cur and (cur.get("type") or cur.get("locator")):
44+
entries.append(cur)
45+
cur = {"name": h.group(1)}
46+
continue
47+
if cur is None:
48+
continue
49+
f = FIELD_RE.match(line)
50+
if f and f.group(1).lower() in FIELDS:
51+
cur[f.group(1).lower()] = f.group(2)
52+
if cur and (cur.get("type") or cur.get("locator")):
53+
entries.append(cur)
54+
return entries
55+
56+
57+
def _parse_date(s):
58+
try:
59+
return datetime.date.fromisoformat((s or "").strip())
60+
except ValueError:
61+
return None
62+
63+
64+
def classify(entries, today):
65+
"""Split entries into (due, pending, undated). `due` = sweep-after reached."""
66+
due, pending, undated = [], [], []
67+
for e in entries:
68+
d = _parse_date(e.get("sweep-after"))
69+
if d is None:
70+
undated.append(e)
71+
elif d <= today:
72+
due.append(e)
73+
else:
74+
pending.append(e)
75+
return due, pending, undated
76+
77+
78+
def main(argv=None):
79+
ap = argparse.ArgumentParser()
80+
ap.add_argument("--manifest", required=True)
81+
ap.add_argument("--json", action="store_true", dest="as_json")
82+
# --today lets a test pin the reference date; defaults to the real today.
83+
ap.add_argument("--today", default="")
84+
a = ap.parse_args(argv)
85+
86+
today = _parse_date(a.today) or datetime.date.today()
87+
try:
88+
with open(a.manifest, encoding="utf-8") as fh:
89+
text = fh.read()
90+
except FileNotFoundError:
91+
text = ""
92+
entries = parse_manifest(text)
93+
due, pending, undated = classify(entries, today)
94+
95+
if a.as_json:
96+
print(json.dumps({
97+
"manifest": a.manifest, "today": today.isoformat(),
98+
"total": len(entries),
99+
"due": due, "pending": pending, "undated": undated,
100+
}))
101+
return 0
102+
103+
print(f"condemned.md: {len(entries)} entr(y/ies) — "
104+
f"{len(due)} due, {len(pending)} pending, {len(undated)} undated "
105+
f"(as of {today.isoformat()})")
106+
for label, group in (("DUE (void)", due), ("pending", pending),
107+
("undated", undated)):
108+
for e in group:
109+
print(f" [{label}] {e['name']} — type={e.get('type','?')} "
110+
f"locator={e.get('locator','?')} "
111+
f"sweep-after={e.get('sweep-after','-')} "
112+
f"archive-ref={e.get('archive-ref','-')}")
113+
return 0
114+
115+
116+
if __name__ == "__main__":
117+
sys.exit(main())

agents/conductors/hygiene/hygiene.sh

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
# hygiene.sh # pre-scan across modes -> ranked worklist (default)
2424
# hygiene.sh perf # import-cost timing (subprocess) -> /refactor + Heart legs
2525
# hygiene.sh perf --profile <script> # cProfile a script, rank NON-likelihood hotspots -> /refactor
26-
# hygiene.sh tidy # git debris pre-scan -> /repo_cleanup
26+
# hygiene.sh tidy # git debris pre-scan -> condemn into condemned.md (async, no per-item gate)
27+
# hygiene.sh sweep # void condemned.md entries past sweep-after -> pyauto-gut void (repo_cleanup gates)
2728
# hygiene.sh noise # CLI-noise route -> /cli_noise_clean
2829
# hygiene.sh deps # dependency-cap pre-scan -> /dep_audit
2930
# hygiene.sh docs # API-docs pre-scan -> /audit_docs
@@ -32,6 +33,12 @@
3233
# hygiene.sh artifacts # tracked leaked outputs/data -> /repo_cleanup
3334
# hygiene.sh <mode> --json # machine-readable HygieneDecision
3435
#
36+
# tidy + sweep are the PyAutoGut drive seam (the organ HOLDS and VOIDS; this
37+
# conductor DECIDES what to condemn and WHEN to sweep, mirroring Heart <-> vitals).
38+
# tidy files 95%-sure debris into the condemned.md manifest asynchronously (no
39+
# synchronous per-item repo_cleanup interrogation); sweep runs the repo_cleanup
40+
# safety gates in batch against entries whose transit window has expired.
41+
#
3542
# All five modes are live. The fast default scan DEFERS perf's import timing (it
3643
# spawns real imports); run `hygiene perf` for it. Repos are read under
3744
# PYAUTO_ROOT (default ~/Code/PyAutoLabs); import timing uses HYGIENE_PYTHON
@@ -49,6 +56,16 @@ LIB_REPOS=(PyAutoConf PyAutoFit PyAutoArray PyAutoGalaxy PyAutoLens)
4956
ORG_REPOS=(PyAutoBrain PyAutoBuild PyAutoHeart PyAutoMind)
5057
DOC_REPOS=(PyAutoFit PyAutoGalaxy PyAutoLens)
5158

59+
# PyAutoGut drive seam (tidy/sweep). The conductor DECIDES and emits a plan; the
60+
# organ entrypoint performs the archive/void. GUT_CMD is referenced in the
61+
# emitted plan, not executed here — the session/organ runs it. MIND holds the
62+
# condemned.md catalog; transit-days is the default holding window a condemned
63+
# item stays recoverable before it is eligible to be swept.
64+
GUT_CMD="${PYAUTO_GUT:-pyauto-gut}"
65+
MIND="$(resolve_mind 2>/dev/null || true)"
66+
CONDEMN_MANIFEST="${MIND:+$MIND/condemned.md}"
67+
CONDEMN_TRANSIT_DAYS="${HYGIENE_CONDEMN_TRANSIT_DAYS:-30}"
68+
5269
# perf: import timing is measured in a subprocess with this interpreter (never
5370
# imported into the conductor). Point HYGIENE_PYTHON at the PyAuto venv to time
5471
# the science libs; HYGIENE_PERF_LIBS overrides the import names (tests use
@@ -60,7 +77,7 @@ read -r -a PERF_LIBS <<< "${HYGIENE_PERF_LIBS:-autoconf autofit autoarray autoga
6077
MODE_ORDER=(perf tidy crlf artifacts noise deps docs config)
6178
declare -A MODE_DELEGATE=(
6279
[perf]="/refactor"
63-
[tidy]="/repo_cleanup"
80+
[tidy]="condemn → condemned.md (async; 'hygiene sweep' voids)"
6481
[crlf]="/refactor"
6582
[artifacts]="/repo_cleanup"
6683
[noise]="/cli_noise_clean"
@@ -243,7 +260,7 @@ mode="default"; json=0; profile_script=""; expect_script=0
243260
for arg in "$@"; do
244261
if [[ "$expect_script" -eq 1 ]]; then profile_script="$arg"; expect_script=0; continue; fi
245262
case "$arg" in
246-
perf|tidy|noise|deps|docs|crlf|config|artifacts) mode="$arg" ;;
263+
perf|tidy|sweep|noise|deps|docs|crlf|config|artifacts) mode="$arg" ;;
247264
default) mode="default" ;;
248265
--json) json=1 ;;
249266
--profile) mode="perf"; expect_script=1 ;;
@@ -298,6 +315,114 @@ if [[ -n "$profile_script" ]]; then
298315
run_profile "$profile_script"; exit $?
299316
fi
300317

318+
# --- PyAutoGut drive seam: tidy (condemn) + sweep (void). ----------------------
319+
# The conductor stays a planner: it enumerates candidates / reads the manifest
320+
# and EMITS the plan (condemned.md entries + the exact pyauto-gut commands). The
321+
# organ performs the archive/void; the session applies the filing. Nothing here
322+
# mutates a repo — consistent with every other hygiene mode.
323+
324+
# enumerate_condemn_candidates — read-only: stale local branches (with merged
325+
# status vs the default branch) and stashes across the managed checkouts. Echoes
326+
# TSV rows: "<repo>\t<type>\t<locator>\t<merged>".
327+
enumerate_condemn_candidates() {
328+
local repo dir def br
329+
for repo in "${LIB_REPOS[@]}" "${ORG_REPOS[@]}"; do
330+
dir="$ROOT/$repo"
331+
[[ -d "$dir/.git" || -f "$dir/.git" ]] || continue
332+
def=$(git -C "$dir" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's@^origin/@@')
333+
[[ -n "$def" ]] || def=main
334+
while IFS= read -r br; do
335+
[[ -z "$br" ]] && continue
336+
case "$br" in main|master|HEAD|"$def") continue ;; esac
337+
local merged=no
338+
if git -C "$dir" merge-base --is-ancestor "$br" "origin/$def" 2>/dev/null \
339+
|| git -C "$dir" merge-base --is-ancestor "$br" "$def" 2>/dev/null; then
340+
merged=yes
341+
fi
342+
printf '%s\tbranch\t%s\t%s\n' "$repo" "$br" "$merged"
343+
done < <(git -C "$dir" for-each-ref --format='%(refname:short)' refs/heads 2>/dev/null)
344+
local i=0 line
345+
while IFS= read -r line; do
346+
[[ -z "$line" ]] && continue
347+
printf '%s\tstash\tstash@{%s}\tno\n' "$repo" "$i"; i=$((i + 1))
348+
done < <(git -C "$dir" stash list 2>/dev/null)
349+
done
350+
}
351+
352+
# run_tidy — emit the async condemnation plan (no synchronous per-item gate).
353+
run_tidy() {
354+
local rows; rows="$(enumerate_condemn_candidates)"
355+
local sweep_after; sweep_after="$(date -d "+${CONDEMN_TRANSIT_DAYS} days" +%F 2>/dev/null || echo "<today+${CONDEMN_TRANSIT_DAYS}d>")"
356+
if [[ "$json" -eq 1 ]]; then
357+
printf '{"decision":"HygieneDecision","mode":"tidy","action":"condemn","manifest":%s,"transit_days":%s,"sweep_after":"%s","candidates":[' \
358+
"$([[ -n "$CONDEMN_MANIFEST" ]] && printf '"%s"' "$CONDEMN_MANIFEST" || echo null)" \
359+
"$CONDEMN_TRANSIT_DAYS" "$sweep_after"
360+
local sep="" repo typ loc merged
361+
while IFS=$'\t' read -r repo typ loc merged; do
362+
[[ -z "$repo" ]] && continue
363+
printf '%s{"repo":"%s","type":"%s","locator":"%s","merged":"%s"}' "$sep" "$repo" "$typ" "$loc" "$merged"; sep=","
364+
done <<< "$rows"
365+
printf ']}\n'; return 0
366+
fi
367+
echo "== HygieneDecision (tidy → condemn) =="
368+
echo "PyAutoGut drive seam: file 95%-sure git debris into condemned.md ASYNC — no"
369+
echo "synchronous per-item gate. Nothing is deleted now; 'hygiene sweep' voids each"
370+
echo "entry once its transit window expires (recover a false positive until then)."
371+
echo
372+
if [[ -z "$rows" ]]; then
373+
echo "No condemn candidates (no stale branches/stashes in the checkouts under $ROOT)."
374+
[[ -z "$MIND" ]] && echo "(PyAutoMind checkout not found — set PYAUTO_MIND to locate condemned.md.)"
375+
return 0
376+
fi
377+
echo "Candidates → one condemned.md entry each (sweep-after ${sweep_after}):"
378+
local repo typ loc merged slug
379+
while IFS=$'\t' read -r repo typ loc merged; do
380+
[[ -z "$repo" ]] && continue
381+
if [[ "$typ" == "branch" && "$merged" == "yes" ]]; then
382+
echo " - ${repo}:${loc} — merged branch → skips the pen: recommend straight delete."
383+
else
384+
slug="${repo,,}-${loc//[^A-Za-z0-9]/-}"
385+
echo " - ${repo}:${loc}${typ}, unmerged → archive then condemn:"
386+
echo " (cd $ROOT/$repo && $GUT_CMD archive ${loc} ${slug})"
387+
echo " condemned.md: type=${typ} locator=${loc} merged=${merged} sweep-after=${sweep_after} archive-ref=refs/heads/archive/condemned/${slug}"
388+
fi
389+
done <<< "$rows"
390+
echo
391+
echo "File the batch async (no per-item interrogation); recover any false positive"
392+
echo "with '$GUT_CMD recover <slug>' until it is swept."
393+
[[ -n "$CONDEMN_MANIFEST" ]] && echo "Manifest: $CONDEMN_MANIFEST"
394+
}
395+
396+
# run_sweep — read condemned.md, emit the batch void plan for past-due entries.
397+
run_sweep() {
398+
if [[ -z "$CONDEMN_MANIFEST" ]]; then
399+
echo "hygiene sweep: PyAutoMind checkout not found — set PYAUTO_MIND to locate condemned.md" >&2
400+
return 4
401+
fi
402+
if [[ "$json" -eq 1 ]]; then
403+
python3 "$HERE/_hygiene_condemned.py" --manifest "$CONDEMN_MANIFEST" --json; return $?
404+
fi
405+
echo "== HygieneDecision (sweep → void) =="
406+
echo "Batch-void condemned.md entries whose transit window has expired. The organ"
407+
echo "performs the deletion; the existing repo_cleanup safety gates apply (no second"
408+
echo "gate). Reabsorb anything still wanted BEFORE sweeping."
409+
echo
410+
python3 "$HERE/_hygiene_condemned.py" --manifest "$CONDEMN_MANIFEST"
411+
echo
412+
local due_names
413+
due_names="$(python3 "$HERE/_hygiene_condemned.py" --manifest "$CONDEMN_MANIFEST" --json \
414+
| python3 -c 'import json,sys;print("\n".join(e["name"] for e in json.load(sys.stdin)["due"]))' 2>/dev/null)"
415+
if [[ -n "$due_names" ]]; then
416+
echo "Void plan (past sweep-after) — run each behind the repo_cleanup gate:"
417+
while IFS= read -r n; do [[ -n "$n" ]] && echo " $GUT_CMD void ${n// /-}"; done <<< "$due_names"
418+
else
419+
echo "Nothing due — no entry has reached its sweep-after date. Pending entries stay recoverable."
420+
fi
421+
}
422+
423+
if [[ "$mode" == "tidy" ]]; then run_tidy; exit $?; fi
424+
if [[ "$mode" == "sweep" ]]; then run_sweep; exit $?; fi
425+
301426
# perf's import timing spawns real imports, so the fast default scan defers it;
302427
# an explicit `hygiene perf` runs it. This predicate decides which.
303428
perf_deferred() { [[ "$1" == "perf" && "$mode" == "default" ]]; }
@@ -340,6 +465,10 @@ echo
340465

341466
render_delegate_line() { # mode
342467
local m="$1"
468+
if [[ "$m" == "tidy" ]]; then
469+
printf ' %-9s %-9s → hygiene tidy condemns these into condemned.md (async); hygiene sweep voids past-due\n' "" ""
470+
return
471+
fi
343472
if [[ "${MODE_KIND[$m]}" == "timing" ]]; then
344473
printf ' %-9s %-9s → route slow items to %s; slow tests/scripts → Heart script_timing/test_run\n' "" "" "${MODE_DELEGATE[$m]}"
345474
else

0 commit comments

Comments
 (0)