From d3b554f6e012267d6c0e3c077ea1163d0c0841bd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 18:44:58 +0000 Subject: [PATCH] lifecycle: add `orphans`, record the M1 rehearsal task, file the triage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three follow-ups to the registry-integrity work (#158). 1. `lifecycle.py orphans` — the mirror of the checks added there. Those validate registry -> prompt; this validates prompt -> registry, listing active/ prompts that no entry claims. A prompt counts as claimed by a resolving `prompt:` path or by an entry whose slug matches its stem, since many entries predate the `prompt:` convention. Report-only, NOT wired into `check`: 8 of 10 active/ prompts are currently unclaimed, and a gate nobody can turn green gets disabled. `--check` exits non-zero for once that backlog is clear. 2. complete/2026/06/build-testpypi-rehearsal-mode.md — the M1 release-rehearsal task, which shipped 2026-06-30 as PyAutoHands #111 (71936fb) and left its prompt stranded in active/ for six weeks. Reconstructed after the fact and labelled as such; the merge body itself names unblocking the Heart release-validation gate (M2). This is the record #158 declined to write before the upstream evidence existed. Folds and removes the active/ prompt. 3. draft/maintenance/pyautomind/active_prompt_orphan_triage.md — the remaining 7 orphans, with per-outcome disposal and the steps to turn `orphans` into a gate afterwards. One is pre-diagnosed: ep_optimise_expose_updater_delta.md belongs to an active.md entry already marked COMPLETE with both PRs merged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GW2wFpSkZtXr8VzM5w8MpX --- active/release_yml_testpypi_rehearsal_mode.md | 70 --------- .../2026/06/build-testpypi-rehearsal-mode.md | 140 ++++++++++++++++++ complete/index.md | 3 +- .../pyautomind/active_prompt_orphan_triage.md | 90 +++++++++++ scripts/lifecycle.py | 63 ++++++++ tests/test_lifecycle_check.py | 43 ++++++ 6 files changed, 338 insertions(+), 71 deletions(-) delete mode 100644 active/release_yml_testpypi_rehearsal_mode.md create mode 100644 complete/2026/06/build-testpypi-rehearsal-mode.md create mode 100644 draft/maintenance/pyautomind/active_prompt_orphan_triage.md diff --git a/active/release_yml_testpypi_rehearsal_mode.md b/active/release_yml_testpypi_rehearsal_mode.md deleted file mode 100644 index fa8bd6cb..00000000 --- a/active/release_yml_testpypi_rehearsal_mode.md +++ /dev/null @@ -1,70 +0,0 @@ -# Add a TestPyPI-only "rehearsal" mode to release.yml - -Type: feature -Target: PyAutoBuild -Repos: -- PyAutoBuild -Status: planned -Difficulty: too-large -Autonomy: supervised -Priority: normal -Milestone: M1 — prerequisite for `feature/pyautoheart/release_validation.md` (M2) - -## Why - -The Heart-owned release-validation pipeline (see -`feature/pyautoheart/release_validation.md`) needs to test the organism against -**built wheels**, not source checkouts — that is the only thing that catches -packaging-layer bugs (a bad `MANIFEST`, a missing data file, a too-loose or -direct-URL dependency). Two real incidents in `complete.md` motivate this: the -PyAutoFit `[nss]` `git+` direct URL silently broke every TestPyPI upload for -weeks, and the nufftax/JAX dependency-floor mismatch produced broken installs — -neither was caught by the source-based validation. - -`release.yml` already knows how to build and publish to TestPyPI (its -`release_test_pypi` job), but that capability is **coupled to the full -release** (TestPyPI → PyPI → tag → notebook commits in one flow). We need to be -able to build + publish the current source to TestPyPI **and stop**, so Heart -can install and validate those wheels before any PyPI promotion. - -## Task - -Add a `rehearsal` (TestPyPI-only) execution mode to `release.yml`: - -- A new `workflow_dispatch` input (e.g. `rehearsal: true`, or reuse/extend the - existing skip flags) that: - - builds every package from current source and **publishes to TestPyPI**, - - then **STOPS** — no PyPI upload, no git tag, no `tag_and_merge`, no notebook - generation/commit to workspaces, no Colab-URL bumps. -- Emit the resolved TestPyPI version string as a workflow output / artifact so - the caller (Heart / Brain health agent) can install exactly those wheels. -- Keep the existing full-release path untouched and default. - -This is intentionally small and isolated — it is the highest-value, lowest-risk -piece and unblocks the rest of the redesign. It does not change the full release -flow; it just exposes "build + TestPyPI, then halt" as a first-class mode. - -## Notes / footguns - -- Respect the "pure executor" boundary: `release.yml` runs no readiness checks; - it just builds/publishes. The gate lives in Heart. -- Verify the TestPyPI upload step tolerates re-runs of the same version - (TestPyPI rejects duplicate filenames) — the rehearsal will be dispatched - repeatedly. Use a dev/local version suffix or `--skip-existing` semantics as - appropriate. -- Confirm no `git+` direct URLs leak into any uploaded wheel's metadata - (the original `[nss]` failure) — a rehearsal that can't upload is the bug - this whole effort exists to surface early. - -## Validation - -- Dispatch the rehearsal mode manually; confirm wheels appear on TestPyPI, the - version string is emitted, and the workflow halts before PyPI/tag/notebook - steps. -- `pytest` in PyAutoBuild stays green. - -## PR - -"PyAutoBuild: TestPyPI-only rehearsal mode in release.yml". - - diff --git a/complete/2026/06/build-testpypi-rehearsal-mode.md b/complete/2026/06/build-testpypi-rehearsal-mode.md new file mode 100644 index 00000000..dbd3de4f --- /dev/null +++ b/complete/2026/06/build-testpypi-rehearsal-mode.md @@ -0,0 +1,140 @@ +# build-testpypi-rehearsal-mode + +- shipped: 2026-06-30 +- pr: https://github.com/PyAutoLabs/PyAutoHands/pull/111 +- commit: `71936fb47c19fd6ba7b64fa9283bf7231e47b165` +- repos: + - PyAutoHands (the prompt targets "PyAutoBuild", this repo's former name) +- milestone: M1 — prerequisite for the Heart release-validation gate (M2) + +## RECORD RECONSTRUCTED AFTER THE FACT (2026-08-08) + +This record was **not** written at ship time by `ship_library`. The task shipped +on 2026-06-30 and nothing in the Mind was updated: the prompt stayed in +`active/`, and `planned.md` went on listing it as `status: planned` for six +weeks. It was found during the registry-integrity audit +(`draft/maintenance/pyautomind/registry_integrity_check.md`) and reconstructed +from upstream git evidence, which is why it carries less detail than a record +written by the ship path — there is no contemporaneous trap/finding log. + +Everything below is verified against PyAutoHands `main`, not inferred. + +## Summary + +Added a TestPyPI-only `rehearsal` mode to `release.yml` so the release pipeline +can build and publish the current source to TestPyPI and **stop** — no PyPI +upload, no git tag, no notebook generation, no Colab-URL bumps. That is what +lets the Heart install and validate the actual wheels before any PyPI +promotion, which is the only way packaging-layer bugs (bad `MANIFEST`, missing +data file, direct-URL dependency) get caught. + +## Evidence it shipped + +In `PyAutoHands/.github/workflows/release.yml` on `main`: + +- a `rehearsal` `workflow_dispatch` input, described in-file as "the one mode + switch: false (default) = a full real release"; +- a `resolve_mode` job publishing `rehearsal` as a job output — the single + source of truth for rehearsal-vs-live; +- a unique PEP 440 dev segment appended per rehearsal run, so repeated + dispatches never collide, plus `--skip-existing` on the upload; +- the PyPI/tag/notebook jobs gated `if: needs.resolve_mode.outputs.rehearsal != 'true'`. + +The merge commit body states the intent directly: *"to unblock the Heart +wheel-based release-validation gate (M2)"*, and notes it was "Validated live on +run #645". + +It has been in production use since: the 2026-08-07 release drive ran Stage 2 +as rehearsal run 31192317261 → `testpypi 2026.8.7.1.dev70601`. + +## Scope delivered beyond the prompt + +The same PR carried a dispatch-surface cleanup the prompt did not ask for: +removed the dead `skip_scripts` / `skip_notebooks` knobs, the redundant +`skip_release` dry-run (rehearsal supersedes it) and the defunct +`update_notebook_visualisations` job, leaving the dispatch surface as +`minor_version + rehearsal`. It also added a pre-upload direct-reference-URL +guard — the `[nss]` `git+` footgun named in the prompt's motivation — and +renamed Pulse→Heart, PyAutoAgent→PyAutoBrain. + +## The milestone chain this belonged to + +M1 of four, all four now shipped and all four discovered stale in `planned.md` +on the same day: M0 `heart-ci-linkage` (PyAutoHeart `heart/checks/ci_status.*`), +M1 this task, M2 `heart-release-validation` (`pyauto-heart validate --ingest`), +M3 `heart-release-profile-wheel-integration` (the named `release` profile in +`heart/validate.py`). The other three had no prompt file to fold and were +removed from `planned.md` rather than recorded. + +## Original prompt + +# Add a TestPyPI-only "rehearsal" mode to release.yml + +Type: feature +Target: PyAutoBuild +Repos: +- PyAutoBuild +Status: planned +Difficulty: too-large +Autonomy: supervised +Priority: normal +Milestone: M1 — prerequisite for `feature/pyautoheart/release_validation.md` (M2) + +## Why + +The Heart-owned release-validation pipeline (see +`feature/pyautoheart/release_validation.md`) needs to test the organism against +**built wheels**, not source checkouts — that is the only thing that catches +packaging-layer bugs (a bad `MANIFEST`, a missing data file, a too-loose or +direct-URL dependency). Two real incidents in `complete.md` motivate this: the +PyAutoFit `[nss]` `git+` direct URL silently broke every TestPyPI upload for +weeks, and the nufftax/JAX dependency-floor mismatch produced broken installs — +neither was caught by the source-based validation. + +`release.yml` already knows how to build and publish to TestPyPI (its +`release_test_pypi` job), but that capability is **coupled to the full +release** (TestPyPI → PyPI → tag → notebook commits in one flow). We need to be +able to build + publish the current source to TestPyPI **and stop**, so Heart +can install and validate those wheels before any PyPI promotion. + +## Task + +Add a `rehearsal` (TestPyPI-only) execution mode to `release.yml`: + +- A new `workflow_dispatch` input (e.g. `rehearsal: true`, or reuse/extend the + existing skip flags) that: + - builds every package from current source and **publishes to TestPyPI**, + - then **STOPS** — no PyPI upload, no git tag, no `tag_and_merge`, no notebook + generation/commit to workspaces, no Colab-URL bumps. +- Emit the resolved TestPyPI version string as a workflow output / artifact so + the caller (Heart / Brain health agent) can install exactly those wheels. +- Keep the existing full-release path untouched and default. + +This is intentionally small and isolated — it is the highest-value, lowest-risk +piece and unblocks the rest of the redesign. It does not change the full release +flow; it just exposes "build + TestPyPI, then halt" as a first-class mode. + +## Notes / footguns + +- Respect the "pure executor" boundary: `release.yml` runs no readiness checks; + it just builds/publishes. The gate lives in Heart. +- Verify the TestPyPI upload step tolerates re-runs of the same version + (TestPyPI rejects duplicate filenames) — the rehearsal will be dispatched + repeatedly. Use a dev/local version suffix or `--skip-existing` semantics as + appropriate. +- Confirm no `git+` direct URLs leak into any uploaded wheel's metadata + (the original `[nss]` failure) — a rehearsal that can't upload is the bug + this whole effort exists to surface early. + +## Validation + +- Dispatch the rehearsal mode manually; confirm wheels appear on TestPyPI, the + version string is emitted, and the workflow halts before PyPI/tag/notebook + steps. +- `pytest` in PyAutoBuild stays green. + +## PR + +"PyAutoBuild: TestPyPI-only rehearsal mode in release.yml". + + diff --git a/complete/index.md b/complete/index.md index ea1ed024..a809f82d 100644 --- a/complete/index.md +++ b/complete/index.md @@ -6,7 +6,7 @@ Token-light navigation over the finished-work records (schema: only then grep a dated bucket. Curators: edit the band between the CURATED markers; everything below GENERATED is rebuilt. -939 records across 7 buckets. +940 records across 7 buckets. ## Highlights @@ -576,6 +576,7 @@ _(curate hard-won records here — survives regeneration.)_ - [autolens-jax-simulator-release-fixes](2026/06/autolens-jax-simulator-release-fixes.md) - [autolens-results-aggregator-valid-dataset](2026/06/autolens-results-aggregator-valid-dataset.md) - [bib-duplicate-keys](2026/06/bib-duplicate-keys.md) +- [build-testpypi-rehearsal-mode](2026/06/build-testpypi-rehearsal-mode.md) - [dark-matter-potentials](2026/06/dark-matter-potentials.md) - [datacube-delaunay-release-memory](2026/06/datacube-delaunay-release-memory.md) - [datacube-shared-state](2026/06/datacube-shared-state.md) diff --git a/draft/maintenance/pyautomind/active_prompt_orphan_triage.md b/draft/maintenance/pyautomind/active_prompt_orphan_triage.md new file mode 100644 index 00000000..7d900714 --- /dev/null +++ b/draft/maintenance/pyautomind/active_prompt_orphan_triage.md @@ -0,0 +1,90 @@ +# Triage the 8 orphaned active/ prompts, then make `orphans` a gate + +Type: maintenance +Target: PyAutoMind +Repos: +- PyAutoMind +Difficulty: medium +Autonomy: supervised +Priority: normal +Status: formalised + +`scripts/lifecycle.py orphans` reports **8 of 10** prompts in `active/` that no +registry entry claims. Each is work whose state nobody is tracking. Triage them, +dispose of each, then flip the reporter into a gate. + +## Background + +`check` gained registry-integrity validation on 2026-08-08 +(`draft/maintenance/pyautomind/registry_integrity_check.md`), covering the +direction **registry → prompt**: every `prompt:` path resolves, exactly, into +the state folder its registry implies. The audit that motivated it found six +`planned.md` entries for work that had already shipped — including the whole +M0–M3 release-validation milestone chain. + +`orphans` is the mirror direction, **prompt → registry**, and it was added in +the same pass but deliberately left **report-only**: it exits 0 unless given +`--check`. Wiring it into `check` immediately would have left the tree red with +8 findings, and a red gate nobody can turn green gets ignored or disabled. The +condition is only worth gating once the backlog is at zero. + +## The inventory (2026-08-08) + +``` +active/benchmark_calibration_runs.md +active/ep_optimise_expose_updater_delta.md +active/euclid_eceb_editorial_revision.md +active/fix_workspace_start_here_colab_links.md +active/matplotlib_inline_standalones.md +active/pyautoreduce_slacs1430_acs_comparison.md +active/research_profiling_experiment_in_the_autolens_pr.md +active/workspace_version.md +``` + +**One is already diagnosed.** `ep_optimise_expose_updater_delta.md` belongs to +the `ep-optimise-updater` entry in `active.md`, which records +`status: COMPLETE 2026-08-08` with both PRs merged (PyAutoFit #1457 → +`3b960609`, autofit_workspace #136 → `cf8b4077`). That is finished work still +sitting in `active.md`, so it wants the normal ship disposal: +`lifecycle.py record ep-optimise-updater --date 2026-08-08 --from-file +--prompt ep_optimise_expose_updater_delta.md --apply`, which writes the record, +folds the prompt and drops the `active.md` section in one step. The entry +already carries enough substance for a rich record. It is an orphan only +because the entry declares no `prompt:` field and the slug does not match the +filename stem. + +The remaining seven need the same question answered individually: **shipped, +still in flight, or abandoned?** Answer it against the upstream repo, not from +the prompt's own text — that is the lesson of the M0–M3 chain, where every +prompt still read as pending while the capability was live on `main`. + +## Disposal per outcome + +- **Shipped** → `lifecycle.py record --date --from-file + --prompt --apply`. If the record has to be reconstructed after + the fact, say so in it and cite the upstream commit/PR — see + `complete/2026/06/build-testpypi-rehearsal-mode.md` for the shape. +- **In flight** → add the missing `active.md` entry, including a `prompt:` + field so the claim is by path rather than by slug coincidence. +- **Abandoned** → `complete/archive/shelved/`, or `condemned.md` if it is + self-material for the Gut's transit-and-void lifecycle. + +## Then close the loop + +Once `lifecycle.py orphans` reports none: + +1. call `orphan_prompts(ROOT)` from `cmd_check` so the condition becomes drift, + and delete the report-only carve-out documented in `cmd_orphans`; +2. move the three orphan tests in `tests/test_lifecycle_check.py` under the + `check` legs alongside the registry ones; +3. consider requiring a `prompt:` field on every `active.md` entry that has a + prompt file. Slug-matching exists only because entries predate the + convention, and it is the weaker claim — `ep-optimise-updater` shows a real + entry and its real prompt failing to match on name alone. + +## Acceptance + +- `python3 scripts/lifecycle.py orphans` prints `none`. +- Every disposal is evidenced: a `complete/` record with an upstream + commit/PR, a new `active.md` entry, or an archive/condemned move. +- `orphans` is called from `cmd_check`, and `pytest tests/` is green. diff --git a/scripts/lifecycle.py b/scripts/lifecycle.py index 2a5d5b3a..653d9f9f 100644 --- a/scripts/lifecycle.py +++ b/scripts/lifecycle.py @@ -38,6 +38,10 @@ * no slug is listed in two registries at once Wire into /health and CI. + orphans [--check] + The mirror of `check`: active/ prompts that no registry entry claims. + Report-only by default (see cmd_orphans for why it is not yet a gate). + This file is intentionally stdlib-only (no PyAuto imports) so it runs in any environment, including a bare template checkout. """ @@ -217,6 +221,60 @@ def registry_problems(root: Path) -> "list[str]": return problems +def orphan_prompts(root: Path) -> "list[Path]": + """active/*.md that no registry entry claims — the mirror of registry_problems(). + + `check` validates registry -> prompt. This is prompt -> registry: a prompt + sitting in active/ that nothing lists is work whose state nobody is + tracking, which is how the M0-M3 release-validation chain shipped without a + single entry being retired. + + A prompt counts as claimed either by a registry `prompt:` path that resolves + to it, or by an entry whose slug matches its filename stem — many entries + predate the `prompt:` convention and identify their file by name alone. + """ + active_dir = root / "active" + if not active_dir.is_dir(): + return [] + + claimed: "set[Path]" = set() + slugs: "set[str]" = set() + for reg in REGISTRY_FILES: + for slug, fields in registry_entries(root / reg): + slugs.add(safe_name(slug)) + raw = fields.get("prompt") + if not raw: + continue + resolved, _ = resolve_prompt(root, raw.split()[0]) + if resolved is not None: + claimed.add(resolved.resolve()) + + orphans = [] + for f in sorted(active_dir.glob("*.md")): + if f.resolve() in claimed or safe_name(f.stem) in slugs: + continue + orphans.append(f) + return orphans + + +def cmd_orphans(args) -> int: + """Report active/ prompts no registry claims. + + Report-only by default and deliberately NOT wired into `check`: the 2026-08-08 + audit found 8 of 10 active/ prompts unclaimed, and each needs individual + triage (shipped? in flight? abandoned?) against its upstream repo before the + condition can be a hard gate. Pass --check once that backlog is cleared. + """ + orphans = orphan_prompts(ROOT) + if not orphans: + print("lifecycle orphans: none") + return 0 + print(f"lifecycle orphans: {len(orphans)} active/ prompt(s) no registry claims") + for f in orphans: + print(f" - {f.relative_to(ROOT)}") + return 1 if getattr(args, "check", False) else 0 + + def _prune_ledger_section(path: Path, slug: str) -> bool: """Drop the `## ` H2 section (heading through the line before the next H2, or EOF) from a ledger file. Returns True if a section was removed.""" @@ -566,6 +624,11 @@ def main() -> int: c = sub.add_parser("check", help="drift guard (non-zero exit on drift)") c.set_defaults(func=cmd_check) + o = sub.add_parser("orphans", help="report active/ prompts no registry claims") + o.add_argument("--check", action="store_true", + help="exit non-zero if any orphan exists (once the backlog is cleared)") + o.set_defaults(func=cmd_orphans) + args = p.parse_args() return args.func(args) diff --git a/tests/test_lifecycle_check.py b/tests/test_lifecycle_check.py index 0c322e0e..d1e5a4dc 100644 --- a/tests/test_lifecycle_check.py +++ b/tests/test_lifecycle_check.py @@ -233,6 +233,49 @@ def test_trailing_parenthetical_after_the_path_is_tolerated(tmp_path): assert lifecycle.registry_problems(root) == [] +# --------------------------------------------------------------------------- # +# the mirror direction — active/ prompts no registry claims +# --------------------------------------------------------------------------- # +def test_unclaimed_active_prompt_is_an_orphan(tmp_path): + root = _tree( + tmp_path, + active=["sprocket_calibration.md", "nobody_tracks_this.md"], + registries={ + "active.md": _entry( + "sprocket-calibration", "active/sprocket_calibration.md" + ) + }, + ) + orphans = [p.name for p in lifecycle.orphan_prompts(root)] + assert orphans == ["nobody_tracks_this.md"] + + +def test_slug_match_claims_a_prompt_without_a_prompt_field(tmp_path): + """Many entries predate the `prompt:` convention and identify their file by + name alone. Requiring `prompt:` would report every one of them as an orphan.""" + root = _tree( + tmp_path, + active=["sprocket_calibration.md"], + registries={"active.md": _entry("sprocket-calibration")}, + ) + assert lifecycle.orphan_prompts(root) == [] + + +def test_a_parked_entry_also_claims_its_active_prompt(tmp_path): + """Started-then-parked work keeps its prompt in active/ while it is listed + in parked.md — that prompt is tracked, not orphaned.""" + root = _tree( + tmp_path, + active=["started_then_parked.md"], + registries={ + "parked.md": _entry( + "started-then-parked", "active/started_then_parked.md" + ) + }, + ) + assert lifecycle.orphan_prompts(root) == [] + + def test_archive_material_does_not_satisfy_a_prompt_path(tmp_path): """complete/archive/ holds retired non-record material and is skipped everywhere else in this module; a shelved copy must not make a missing